From 2f767f566a8fd87c0028d568b068688917565cfd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Jul 2025 19:29:43 +0500 Subject: [PATCH 001/165] Updated on 2026-08-14 --- .../src/main/res/drawable/ic_stack_new_24.xml | 16 ++++++++++++ .../impl/amount/SwapAmountBlockComponent.kt | 4 +++ .../v2/impl/amount/model/SwapAmountModel.kt | 7 ++++++ .../SwapChooseProviderComponent.kt | 4 ++- .../SwapChooseProviderBottomSheetContent.kt | 1 + .../model/SwapChooseProviderModel.kt | 6 +++++ .../SwapProviderListItemConverter.kt | 10 +++++++- .../ui/SwapChooseProviderBottomSheet.kt | 25 +++++++++++++++---- .../SwapChooseProviderContentPreview.kt | 3 ++- .../swap/v2/impl/common/SwapProviderUtils.kt | 13 ++++++++++ 10 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_stack_new_24.xml create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapProviderUtils.kt diff --git a/core/ui/src/main/res/drawable/ic_stack_new_24.xml b/core/ui/src/main/res/drawable/ic_stack_new_24.xml new file mode 100644 index 0000000000..a0a35fc823 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_stack_new_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt index dbbf511732..5917c12bf3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams.AmountBlockParams import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel @@ -77,6 +78,7 @@ internal class SwapAmountBlockComponent( providers = amountUM.swapQuotes, cryptoCurrency = cryptoCurrency, selectedProvider = selectedProvider, + userCountry = model.userCountry, ), ) }, @@ -95,6 +97,7 @@ internal class SwapAmountBlockComponent( providers = config.providers, cryptoCurrency = config.cryptoCurrency, selectedProvider = config.selectedProvider, + userCountry = config.userCountry, callback = model, onDismiss = { model.bottomSheetNavigation.dismiss() }, ), @@ -105,5 +108,6 @@ internal class SwapAmountBlockComponent( val providers: ImmutableList, val cryptoCurrency: CryptoCurrency, val selectedProvider: ExpressProvider, + val userCountry: UserCountry, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index dfc08a58f3..23c478c056 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection @@ -54,6 +56,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal +import java.util.Locale import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -69,6 +72,7 @@ internal class SwapAmountModel @Inject constructor( private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, private val getAllowanceUseCase: GetAllowanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserCountryUseCase: GetUserCountryUseCase, private val appRouter: AppRouter, private val swapAmountAlertFactory: SwapAmountAlertFactory, private val swapAlertFactory: SwapAlertFactory, @@ -89,6 +93,7 @@ internal class SwapAmountModel @Inject constructor( private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null + var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() val uiState: StateFlow @@ -100,6 +105,8 @@ internal class SwapAmountModel @Inject constructor( init { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + userCountry = getUserCountryUseCase.invokeSync().getOrNull() + ?: UserCountry.Other(Locale.getDefault().country) } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt index 744a8a3c91..c04a88654c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.features.swap.v2.impl.chooseprovider.model.SwapChooseProviderModel import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderBottomSheet import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -41,7 +42,7 @@ internal class SwapChooseProviderComponent( SwapChooseProviderBottomSheet(config = bottomSheetConfig) { SwapChooseProviderContent( - providerList = state.value.providerList, + contentUM = state.value, onProviderClick = model::onProviderClick, ) } @@ -51,6 +52,7 @@ internal class SwapChooseProviderComponent( val cryptoCurrency: CryptoCurrency, val selectedProvider: ExpressProvider, val providers: ImmutableList, + val userCountry: UserCountry, val callback: ModelCallback, val onDismiss: () -> Unit, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt index ad5eb47e18..289afe2a26 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt @@ -7,6 +7,7 @@ import kotlinx.collections.immutable.ImmutableList internal data class SwapChooseProviderBottomSheetContent( val providerList: ImmutableList, + val isApplyFCARestrictions: Boolean, val selectedProvider: ExpressProvider, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index 5b9ce17796..c4bf3d574e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -3,10 +3,12 @@ package com.tangem.features.swap.v2.impl.chooseprovider.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProviderListItemConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow @@ -21,10 +23,13 @@ internal class SwapChooseProviderModel @Inject constructor( private val params: SwapChooseProviderComponent.Params = paramsContainer.require() + private val needApplyFCARestrictions = params.userCountry.needApplyFCARestrictions() + private val swapProviderListItemConverter by lazy(LazyThreadSafetyMode.NONE) { SwapProviderListItemConverter( cryptoCurrency = params.cryptoCurrency, selectedProvider = params.selectedProvider, + needApplyFCARestrictions = needApplyFCARestrictions, ) } @@ -38,6 +43,7 @@ internal class SwapChooseProviderModel @Inject constructor( private fun getInitialState(): SwapChooseProviderBottomSheetContent { return SwapChooseProviderBottomSheetContent( + isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), providerList = swapProviderListItemConverter.convertList(params.providers) .filterNotNull() .toPersistentList(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 75234f816b..e7f45e0726 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -15,11 +15,13 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.converter.Converter internal class SwapProviderListItemConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, + private val needApplyFCARestrictions: Boolean, ) : Converter { override fun convert(value: SwapQuoteUM): SwapProviderListItem? { val provider = value.provider ?: return null @@ -66,9 +68,15 @@ internal class SwapProviderListItemConverter( }, ) } + is SwapQuoteUM.Content -> if (needApplyFCARestrictions && value.provider.isRestrictedByFCA()) { + ProviderChooseUM.ExtraUM.Action( + text = resourceReference(R.string.express_provider_fca_warning_list), + ) + } else { + ProviderChooseUM.ExtraUM.Empty + } SwapQuoteUM.Empty, SwapQuoteUM.Loading, - is SwapQuoteUM.Content, -> ProviderChooseUM.ExtraUM.Empty } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 46ab92931b..11d0e32879 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -19,6 +20,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.provider.ProviderChooseCrypto import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resourceReference @@ -27,10 +29,9 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent -import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem import com.tangem.features.swap.v2.impl.chooseprovider.ui.preview.SwapChooseProviderContentPreview import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -import kotlinx.collections.immutable.ImmutableList +import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM @Composable internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { @@ -50,7 +51,7 @@ internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, cont @Composable internal fun SwapChooseProviderContent( - providerList: ImmutableList, + contentUM: SwapChooseProviderBottomSheetContent, onProviderClick: (SwapQuoteUM) -> Unit, modifier: Modifier = Modifier, ) { @@ -64,7 +65,17 @@ internal fun SwapChooseProviderContent( color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, ) - providerList.fastForEachIndexed { index, provider -> + AnimatedVisibility( + modifier = Modifier.padding(top = 12.dp), + visible = contentUM.isApplyFCARestrictions, + ) { + Notification( + config = SwapNotificationUM.Error.FCAWarningList.config, + containerColor = TangemTheme.colors.button.disabled, + iconTint = TangemTheme.colors.icon.warning, + ) + } + contentUM.providerList.fastForEachIndexed { index, provider -> ProviderChooseCrypto( providerChooseUM = provider.providerUM, onClick = { onProviderClick(provider.quote) }, @@ -114,7 +125,11 @@ private fun SwapChooseProviderContent_Preview( ), ) { SwapChooseProviderContent( - providerList = params.providerList, + contentUM = SwapChooseProviderBottomSheetContent( + providerList = params.providerList, + isApplyFCARestrictions = true, + selectedProvider = SwapChooseProviderContentPreview.provider1, + ), onProviderClick = {}, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index b96c7acb4b..dd75bbf62c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -16,7 +16,7 @@ import java.math.BigDecimal internal object SwapChooseProviderContentPreview { - private val provider1 = ExpressProvider( + val provider1 = ExpressProvider( providerId = "changenow", rateTypes = listOf(ExpressRateType.Float), name = "ChangeNow", @@ -88,5 +88,6 @@ internal object SwapChooseProviderContentPreview { ), ), selectedProvider = provider1, + isApplyFCARestrictions = false, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapProviderUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapProviderUtils.kt new file mode 100644 index 0000000000..6c9e8ece21 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapProviderUtils.kt @@ -0,0 +1,13 @@ +package com.tangem.features.swap.v2.impl.common + +import com.tangem.domain.express.models.ExpressProvider + +private val FCA_RESTRICTED_PROVIDER_IDS = setOf( + "changelly", + "changenow", + "okx-cross-chain", + "okx-on-chain", + "simpleswap", +) + +fun ExpressProvider.isRestrictedByFCA() = FCA_RESTRICTED_PROVIDER_IDS.contains(providerId) \ No newline at end of file From cf559c1a11aa1ae17ba231e9ea88c3bd2df5602d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Jul 2025 20:46:15 +0500 Subject: [PATCH 002/165] Updated on 2026-08-14 --- .../provider/ProviderChooseCrypto.kt | 16 +- .../SwapAmountSetQuotesTransformer.kt | 1 + .../SwapChooseProviderBottomSheetContent.kt | 1 + .../entity/SwapProviderState.kt | 34 +++ .../model/SwapChooseProviderModel.kt | 10 +- .../SwapProviderListItemConverter.kt | 10 + .../converter/SwapProviderStateConverter.kt | 92 +++++++ .../ui/SwapChooseProviderBottomSheet.kt | 26 +- .../ui/SwapChooseProviderContent.kt | 21 +- .../chooseprovider/ui/SwapProviderItem.kt | 232 ++++++++++++++++++ .../SwapChooseProviderContentPreview.kt | 19 ++ .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 1 + 12 files changed, 443 insertions(+), 20 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index aa7f0f5d08..0370272dcc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.Visibility import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.audits.AuditLabel import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.badge.Badge @@ -50,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { ConstraintLayout( modifier = modifier - .background(TangemTheme.colors.background.action) .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = providerChooseUM.isSelected) .clickable( @@ -133,13 +133,23 @@ private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) { SubcomposeAsyncImage( modifier = modifier .size(40.dp) - .clip(RoundedCornerShape(8.dp)) - .background(TangemColorPalette.Light1), + .clip(RoundedCornerShape(8.dp)), model = ImageRequest.Builder(context = LocalContext.current) .data(iconUrl) .crossfade(enable = true) .allowHardware(false) .build(), + loading = { + RectangleShimmer(radius = 8.dp) + }, + error = { + Box( + modifier = Modifier.background( + color = TangemColorPalette.Light1, + shape = RoundedCornerShape(8.dp), + ), + ) + }, contentDescription = null, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index e6b6c96434..614f1d8b53 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -58,6 +58,7 @@ internal class SwapAmountSetQuotesTransformer( val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE quote.copy( diffPercent = DifferencePercent.Diff( + isPositive = percent.isPositive(), percent = stringReference( if (percent.isPositive()) { "${StringsSigns.PLUS}${percent.format { percent() }}" diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt index 289afe2a26..7be1b3c0c7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt @@ -13,5 +13,6 @@ internal data class SwapChooseProviderBottomSheetContent( internal data class SwapProviderListItem( val providerUM: ProviderChooseUM, + val swapProviderState: SwapProviderState, val quote: SwapQuoteUM, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt new file mode 100644 index 0000000000..24e18c0fb1 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapProviderState.kt @@ -0,0 +1,34 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent + +@Deprecated("Use ProviderChooseUM with new design") +@Immutable +internal sealed class SwapProviderState { + + abstract val isSelected: Boolean + + data object Empty : SwapProviderState() { + override val isSelected = false + } + + data class Content( + override val isSelected: Boolean, + val name: String, + val type: String, + val iconUrl: String, + val subtitle: TextReference, + val additionalBadge: AdditionalBadge, + val diffPercent: DifferencePercent, + ) : SwapProviderState() + + @Immutable + sealed class AdditionalBadge { + data object FCAWarningList : AdditionalBadge() + data object BestTrade : AdditionalBadge() + data object Empty : AdditionalBadge() + data object PermissionRequired : AdditionalBadge() + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index c4bf3d574e..08511759a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.swap.v2.impl.chooseprovider.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.express.models.ExpressError import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent @@ -10,6 +11,7 @@ import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProvi import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -30,6 +32,7 @@ internal class SwapChooseProviderModel @Inject constructor( cryptoCurrency = params.cryptoCurrency, selectedProvider = params.selectedProvider, needApplyFCARestrictions = needApplyFCARestrictions, + needBestRateBadge = params.providers.filterIsInstance().isSingleItem().not(), ) } @@ -42,9 +45,14 @@ internal class SwapChooseProviderModel @Inject constructor( } private fun getInitialState(): SwapChooseProviderBottomSheetContent { + val filteredProviderList = params.providers.filter { + it is SwapQuoteUM.Content || + it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + } return SwapChooseProviderBottomSheetContent( isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), - providerList = swapProviderListItemConverter.convertList(params.providers) + providerList = swapProviderListItemConverter.convertList(filteredProviderList) .filterNotNull() .toPersistentList(), selectedProvider = params.selectedProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index e7f45e0726..045d882f8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -22,11 +22,21 @@ internal class SwapProviderListItemConverter( private val cryptoCurrency: CryptoCurrency, private val selectedProvider: ExpressProvider, private val needApplyFCARestrictions: Boolean, + needBestRateBadge: Boolean, ) : Converter { + + private val providerStateConverter = SwapProviderStateConverter( + cryptoCurrency = cryptoCurrency, + selectedProvider = selectedProvider, + needApplyFCARestrictions = needApplyFCARestrictions, + isNeedBestRateBadge = needBestRateBadge, + ) + override fun convert(value: SwapQuoteUM): SwapProviderListItem? { val provider = value.provider ?: return null return SwapProviderListItem( + swapProviderState = providerStateConverter.convert(value), providerUM = ProviderChooseUM( title = stringReference(provider.name), subtitle = stringReference(provider.type.typeName), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt new file mode 100644 index 0000000000..4a7a127735 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.model.converter + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState.AdditionalBadge +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA +import com.tangem.utils.converter.Converter + +@Deprecated("Remove with new design") +internal class SwapProviderStateConverter( + private val cryptoCurrency: CryptoCurrency, + private val selectedProvider: ExpressProvider, + private val isNeedBestRateBadge: Boolean, + private val needApplyFCARestrictions: Boolean, +) : Converter { + + override fun convert(value: SwapQuoteUM): SwapProviderState { + return when (value) { + is SwapQuoteUM.Content -> value.convertToContent() + is SwapQuoteUM.Error -> value.convertToErrorContent() + is SwapQuoteUM.Allowance, + SwapQuoteUM.Empty, + SwapQuoteUM.Loading, + -> SwapProviderState.Empty + } + } + + private fun SwapQuoteUM.Content.convertToContent(): SwapProviderState { + val isBestRate = when (diffPercent) { + SwapQuoteUM.Content.DifferencePercent.Best -> true + else -> false + } + + val additionalBadge = when { + needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> AdditionalBadge.BestTrade + else -> AdditionalBadge.Empty + } + + return SwapProviderState.Content( + name = provider.name, + iconUrl = provider.imageLarge, + type = provider.type.typeName, + subtitle = quoteAmountValue, + additionalBadge = additionalBadge, + diffPercent = diffPercent, + isSelected = provider == selectedProvider, + ) + } + + private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState { + val additionalBadge = when { + needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList + else -> AdditionalBadge.Empty + } + + return SwapProviderState.Content( + name = provider.name, + iconUrl = provider.imageLarge, + type = provider.type.typeName, + subtitle = when (val error = expressError) { + is ExpressError.AmountError.TooSmallError -> resourceReference( + id = R.string.express_provider_min_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + is ExpressError.AmountError.NotEnoughAllowanceError, + is ExpressError.AmountError.TooBigError, + -> resourceReference( + id = R.string.express_provider_max_amount, + formatArgs = wrappedList( + error.amount.format { crypto(cryptoCurrency) }, + ), + ) + else -> TextReference.EMPTY + }, + additionalBadge = additionalBadge, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSelected = false, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 11d0e32879..dcd529b3f9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -2,13 +2,16 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text 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.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -16,14 +19,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.provider.ProviderChooseCrypto -import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -57,13 +60,14 @@ internal fun SwapChooseProviderContent( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 13.dp), + modifier = modifier.padding(horizontal = 13.dp), ) { Text( text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 4.dp), ) AnimatedVisibility( modifier = Modifier.padding(top = 12.dp), @@ -75,12 +79,18 @@ internal fun SwapChooseProviderContent( iconTint = TangemTheme.colors.icon.warning, ) } + SpacerH12() contentUM.providerList.fastForEachIndexed { index, provider -> - ProviderChooseCrypto( - providerChooseUM = provider.providerUM, - onClick = { onProviderClick(provider.quote) }, - modifier = modifier - .conditional(index == 0) { padding(top = 24.dp) }, + SwapProviderItem( + state = provider.swapProviderState, + modifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .selectedBorder(isSelected = provider.swapProviderState.isSelected) + .clickable( + enabled = provider.quote !is SwapQuoteUM.Error, + onClick = { onProviderClick(provider.quote) }, + ) + .padding(12.dp), ) } Icon( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 43c2d3c8f0..38a3b4718c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -4,11 +4,7 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -27,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette @@ -57,7 +54,7 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> ) { Icon( painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_exchange_horizontal_24), + ImageVector.vectorResource(R.drawable.ic_stack_new_24), ), tint = TangemTheme.colors.icon.accent, contentDescription = null, @@ -72,13 +69,21 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> SubcomposeAsyncImage( modifier = modifier .size(20.dp) - .clip(RoundedCornerShape(4.dp)) - .background(TangemColorPalette.Light1), + .clip(RoundedCornerShape(4.dp)), model = ImageRequest.Builder(context = LocalContext.current) .data(expressProvider?.imageLarge) .crossfade(enable = true) .allowHardware(false) .build(), + loading = { RectangleShimmer(radius = 4.dp) }, + error = { + Box( + modifier = Modifier.background( + color = TangemColorPalette.Light1, + shape = RoundedCornerShape(4.dp), + ), + ) + }, contentDescription = null, ) Text( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt new file mode 100644 index 0000000000..4b8df6d2b6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapProviderItem.kt @@ -0,0 +1,232 @@ +package com.tangem.features.swap.v2.impl.chooseprovider.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM + +@Deprecated("Use ProviderChooseCrypto with new design") +@Composable +internal fun SwapProviderItem(state: SwapProviderState, modifier: Modifier = Modifier) { + when (state) { + is SwapProviderState.Content -> ProviderContentState( + state = state, + modifier = modifier, + ) + is SwapProviderState.Empty -> { /* no-op */ + } + } +} + +@Suppress("LongMethod") +@Composable +private fun ProviderContentState(state: SwapProviderState.Content, modifier: Modifier = Modifier) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = 40.dp) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl) + .crossfade(enable = true).allowHardware(false).build(), + loading = { RectangleShimmer(radius = 8.dp) }, + error = { + ErrorProviderIcon(Modifier.size(size = 40.dp)) + }, + contentDescription = null, + ) + + Column(modifier = Modifier.padding(start = 12.dp)) { + Row { + Text( + text = state.name, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.type, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = 4.dp), + ) + val badgeModifier = Modifier.padding(start = 4.dp) + when (state.additionalBadge) { + SwapProviderState.AdditionalBadge.FCAWarningList -> FCABadgeItem(badgeModifier) + SwapProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier) + SwapProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier) + SwapProviderState.AdditionalBadge.Empty -> Unit + } + } + Row( + modifier = Modifier.padding(top = 2.dp), + ) { + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + if (state.diffPercent is SwapQuoteUM.Content.DifferencePercent.Diff) { + val textColor = if (state.diffPercent.isPositive) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.text.warning + } + + Text( + text = state.diffPercent.percent.resolveReference(), + style = TangemTheme.typography.body2, + color = textColor, + modifier = Modifier.padding(start = 4.dp), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } + } + } +} + +@Composable +private fun ErrorProviderIcon(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCorners8, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + contentDescription = null, + ) + } +} + +@Composable +private fun BestTradeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +@Composable +private fun PermissionBadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(id = R.string.express_provider_permission_needed), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +@Composable +private fun FCABadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResourceSafe(id = R.string.express_provider_fca_warning_list), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = 6.dp), + maxLines = 1, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ProviderItemPreview( + @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, +) { + TangemThemePreview { + SwapProviderItem( + modifier = Modifier.background(TangemTheme.colors.background.action), + state = state.first, + ) + } +} + +private class ProviderItemParameterProvider : CollectionPreviewParameterProvider>( + collection = buildList { + val contentState = SwapProviderState.Content( + name = "1inch", + type = "DEX", + iconUrl = "", + subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"), + additionalBadge = SwapProviderState.AdditionalBadge.Empty, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff( + isPositive = false, + percent = stringReference("-10%"), + ), + isSelected = true, + ) + val contentState2 = contentState.copy( + subtitle = stringReference(value = "1 132,46 MATIC"), + additionalBadge = SwapProviderState.AdditionalBadge.PermissionRequired, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff( + isPositive = true, + percent = stringReference("+10%"), + ), + ) + add(contentState to true) + add(contentState to false) + + add(contentState2 to true) + add(contentState2 to false) + }, +) +// endregion Preview \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index dd75bbf62c..0975e02bdf 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -10,6 +10,7 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem +import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -65,6 +66,15 @@ internal object SwapChooseProviderContentPreview { ), ), ), + swapProviderState = SwapProviderState.Content( + name = provider1.name, + type = provider1.type.typeName, + iconUrl = "", + subtitle = stringReference("1800 POL"), + additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSelected = true, + ), quote = quote1, ), SwapProviderListItem( @@ -85,6 +95,15 @@ internal object SwapChooseProviderContentPreview { ), ), quote = quote2, + swapProviderState = SwapProviderState.Content( + name = provider1.name, + type = provider1.type.typeName, + iconUrl = "", + subtitle = stringReference("1800 POL"), + additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, + diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSelected = true, + ), ), ), selectedProvider = provider1, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index ec6aaf74e7..ff661ba3ee 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -40,6 +40,7 @@ internal sealed class SwapQuoteUM { data object Empty : DifferencePercent() data object Best : DifferencePercent() data class Diff( + val isPositive: Boolean, val percent: TextReference, ) : DifferencePercent() } From 496072829c204c13d1fc630490dff3ccb2902b43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Jul 2025 13:44:57 +0400 Subject: [PATCH 003/165] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 24 +----------- .../data/staking/DefaultStakingRepository.kt | 31 ++-------------- .../com/tangem/data/swap/di/SwapDataModule.kt | 4 +- .../GetActionRequirementAmountUseCase.kt | 18 +++++---- .../staking/GetStakingIntegrationIdUseCase.kt | 12 ------ .../domain/staking/IsApproveNeededUseCase.kt | 19 ---------- .../staking/repositories/StakingRepository.kt | 6 --- .../BaseCurrencyStatusOperations.kt | 2 +- .../CachedCurrenciesStatusesOperations.kt | 8 ++-- .../utils/CurrencyStatusProxyCreator.kt | 13 ++----- .../impl/presentation/model/StakingModel.kt | 37 +++++++++---------- .../tokendetails/model/TokenDetailsModel.kt | 7 +--- .../TokenDetailsSkeletonStateConverter.kt | 5 +-- .../state/factory/TokenDetailsStateFactory.kt | 7 +--- 14 files changed, 47 insertions(+), 146 deletions(-) delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index b6152265bf..213e3753c7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -173,18 +173,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideIsApproveNeededUseCase( - stakingRepository: StakingRepository, - stakingErrorResolver: StakingErrorResolver, - ): IsApproveNeededUseCase { - return IsApproveNeededUseCase( - stakingRepository = stakingRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideGetConstructedStakingTransactionUseCase( @@ -209,12 +197,6 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase { - return GetStakingIntegrationIdUseCase(stakingRepository) - } - @Provides @Singleton fun provideCheckAccountInitializedUseCase( @@ -225,9 +207,7 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetActionRequirementAmountUseCase( - stakingRepository: StakingRepository, - ): GetActionRequirementAmountUseCase { - return GetActionRequirementAmountUseCase(stakingRepository) + fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase { + return GetActionRequirementAmountUseCase() } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index fbdea80804..15787789bf 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -10,8 +10,6 @@ import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.blockchainsdk.utils.toMigratedCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.staking.converters.YieldConverter @@ -38,9 +36,9 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -63,7 +61,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal -import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( @@ -95,10 +92,6 @@ internal class DefaultStakingRepository( private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } - override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? { - return stakingIdFactory.createIntegrationId(currencyId = cryptoCurrencyId) - } - override suspend fun fetchEnabledYields() { withContext(dispatchers.io) { when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) { @@ -202,7 +195,7 @@ internal class DefaultStakingRepository( return@channelFlow } - val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null getEnabledYields() .distinctUntilChanged() @@ -248,7 +241,7 @@ internal class DefaultStakingRepository( return StakingAvailability.Unavailable } - val isSupportedInMobileApp = getSupportedIntegrationId(cryptoCurrency.id).isNullOrEmpty().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = cryptoCurrency.id) != null val yields = getEnabledYieldsSync() if (yields.isEmpty()) { @@ -481,17 +474,6 @@ internal class DefaultStakingRepository( } } - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - val integrationId = stakingIdFactory.createIntegrationId(currencyId = cryptoCurrency.id) - - return when (integrationId) { - Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(), - Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(), - -> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) - else -> StakingApproval.Empty - } - } - private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { return when (Blockchain.fromId(networkId)) { Blockchain.Solana, @@ -543,14 +525,7 @@ internal class DefaultStakingRepository( } } - @Suppress("unused") companion object { - private const val YIELDS_STORE_KEY = "yields" - - private const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" - - internal val YIELDS_WATITING_TIMEOUT = 15.seconds - private val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") } } \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index b7df9273ac..5edf45f002 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -14,7 +14,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressRepository import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.SwapTransactionRepository @@ -49,7 +48,6 @@ internal object SwapDataModule { dataSignatureVerifier: DataSignatureVerifier, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleQuoteStatusFetcher: SingleQuoteStatusFetcher, - stakingRepository: StakingRepository, @NetworkMoshi moshi: Moshi, ): SwapRepositoryV2 { return DefaultSwapRepositoryV2( @@ -61,7 +59,7 @@ internal object SwapDataModule { moshi = moshi, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleQuoteStatusFetcher = singleQuoteStatusFetcher, - currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository), + currencyStatusProxyCreator = CurrencyStatusProxyCreator(), ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt index 34d0c03b27..ccbb011f35 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt @@ -1,16 +1,18 @@ package com.tangem.domain.staking -import arrow.core.Either +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.repositories.StakingRepository import java.math.BigDecimal -class GetActionRequirementAmountUseCase( - private val stakingRepository: StakingRepository, -) { +class GetActionRequirementAmountUseCase { - operator fun invoke(integrationId: String, actionType: StakingActionType): Either = - Either.catch { - stakingRepository.getActionRequirementAmount(integrationId, actionType) + operator fun invoke(integrationId: String, actionType: StakingActionType): BigDecimal? { + return if (StakingIntegrationID.EthereumToken.Polygon.value == integrationId && + actionType == StakingActionType.CLAIM_REWARDS + ) { + BigDecimal.ONE + } else { + null } + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt deleted file mode 100644 index 2c9d213e72..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingIntegrationIdUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.staking - -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.repositories.StakingRepository - -class GetStakingIntegrationIdUseCase( - private val stakingRepository: StakingRepository, -) { - - operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID) = - stakingRepository.getSupportedIntegrationId(cryptoCurrencyId) -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt deleted file mode 100644 index 549dbb614c..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsApproveNeededUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingRepository - -class IsApproveNeededUseCase( - private val stakingRepository: StakingRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - operator fun invoke(cryptoCurrency: CryptoCurrency): Either { - return Either - .catch { stakingRepository.getStakingApproval(cryptoCurrency) } - .mapLeft { stakingErrorResolver.resolve(it) } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index dce26456cb..dcb09a9010 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -6,7 +6,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.NetworkType @@ -24,8 +23,6 @@ import java.math.BigDecimal @Suppress("TooManyFunctions") interface StakingRepository { - fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? - suspend fun fetchEnabledYields() suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo @@ -66,9 +63,6 @@ interface StakingRepository { transactionId: String, ): Pair - /** Returns staking approval */ - fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval - suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 80744461c7..351c2e0f89 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -52,7 +52,7 @@ abstract class BaseCurrencyStatusOperations( private val tokensFeatureToggles: TokensFeatureToggles, ) { - protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) + protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 2861f1eb19..6bcb07b46b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -24,8 +24,8 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalance.Unsupported.integrationId import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceProducer @@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.* class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - private val stakingRepository: StakingRepository, + stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -270,9 +270,7 @@ class CachedCurrenciesStatusesOperations( ): YieldBalance? { if (yieldBalances.isNullOrEmpty()) return null - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - - if (supportedIntegration.isNullOrBlank()) return null + val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value ?: return null val address = extractAddress(networkStatus) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index f6e74db9ca..c1778e6af1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -7,9 +7,8 @@ import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalance.Unsupported.integrationId -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations @@ -17,13 +16,9 @@ import com.tangem.domain.tokens.operations.CurrencyStatusOperations /** * Proxy creator of [CryptoCurrencyStatus]. Used [CurrencyStatusOperations] to create statuses. * - * @property stakingRepository staking repository - * [REDACTED_AUTHOR] */ -class CurrencyStatusProxyCreator( - private val stakingRepository: StakingRepository, -) { +class CurrencyStatusProxyCreator { fun createCurrencyStatus( currency: CryptoCurrency, @@ -78,8 +73,8 @@ class CurrencyStatusProxyCreator( val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } val address = extractAddress(networkStatus) - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - val yieldBalance = if (supportedIntegration.isNullOrEmpty().not()) { + val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value + val yieldBalance = if (supportedIntegration != null) { yieldBalances?.firstOrNull { it.integrationId == supportedIntegration && it.address == address } ?: YieldBalance.Error(integrationId = supportedIntegration, address = address) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 8b99cc5560..96e5279cb3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -29,10 +29,14 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.staking.* import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -45,9 +49,6 @@ import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor @@ -103,7 +104,6 @@ internal class StakingModel @Inject constructor( private val sendTransactionUseCase: SendTransactionUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val getAllowanceUseCase: GetAllowanceUseCase, - private val isApproveNeededUseCase: IsApproveNeededUseCase, private val vibratorHapticManager: VibratorHapticManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, @@ -534,7 +534,7 @@ internal class StakingModel @Inject constructor( getActionRequirementAmountUseCase.invoke( integrationId = yieldBalance.integrationId, actionType = StakingActionType.CLAIM_REWARDS, - ).getOrNull() + ) } else { minimumAmount } @@ -904,21 +904,18 @@ internal class StakingModel @Inject constructor( } private suspend fun setupApprovalNeeded() { - stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).fold( - ifRight = { approval -> - if (approval is StakingApproval.Needed) { - stakingAllowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - } - approval - }, - ifLeft = { - StakingApproval.Empty - }, - ) + val approval = StakingIntegrationID.create(currencyId = cryptoCurrencyStatus.currency.id)?.approval + ?: StakingApproval.Empty + + stakingApproval = approval + + if (approval is StakingApproval.Needed) { + stakingAllowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + } } private suspend fun setupIsAnyTokenStaked() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 186939ab0a..cb0d326470 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -38,13 +38,14 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoTokenUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* @@ -67,8 +68,6 @@ import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase @@ -133,7 +132,6 @@ internal class TokenDetailsModel @Inject constructor( paramsContainer: ParamsContainer, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, - getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, @@ -169,7 +167,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index ac4a41d6a3..3b3acb4d24 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -11,8 +11,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, - private val getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, ) : Converter { @@ -38,7 +37,7 @@ internal class TokenDetailsSkeletonStateConverter( override fun convert(value: CryptoCurrency): TokenDetailsState { val iconState = iconStateConverter.convert(value) - val isSupportedInMobileApp = getStakingIntegrationIdUseCase(value.id).isNullOrBlank().not() + val isSupportedInMobileApp = StakingIntegrationID.create(currencyId = value.id) != null return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 16726e8a98..251bfb5dfd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -18,7 +18,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.staking.GetStakingIntegrationIdUseCase +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.error.CurrencyStatusError @@ -28,8 +29,6 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -55,7 +54,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, symbol: String, decimals: Int, ) { @@ -64,7 +62,6 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, networkHasDerivationUseCase = networkHasDerivationUseCase, - getStakingIntegrationIdUseCase = getStakingIntegrationIdUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, ) From 8ec30bd373330ac811f212e6eaa74a779ae455c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Jul 2025 11:26:48 +0300 Subject: [PATCH 004/165] Updated on 2026-08-14 --- .../com/tangem/common/utils/WireMockUtils.kt | 96 ++++ .../screens/BuyTokenDetailsPageObject.kt | 101 ++++ .../screens/BuyTokenFiatListPageObject.kt | 38 ++ .../com/tangem/screens/BuyTokenPageObject.kt | 54 ++ .../com/tangem/screens/DialogPageObject.kt | 10 +- .../tangem/screens/MainScreenPageObject.kt | 5 + .../screens/ResidenceSettingsPageObject.kt | 46 ++ .../tangem/screens/SelectCountryPageObject.kt | 59 +++ .../screens/SelectPaymentMethodPageObject.kt | 50 ++ .../screens/SelectProviderPageObject.kt | 91 ++++ .../kotlin/com/tangem/tests/BuyTokenTest.kt | 469 ++++++++++++++++++ .../kotlin/com/tangem/tests/HideTokenTest.kt | 2 +- .../ui/components/appbar/TangemTopAppBar.kt | 5 + .../components/buttons/common/TangemButton.kt | 4 +- .../core/ui/components/fields/SearchBar.kt | 5 +- .../components/notifications/Notification.kt | 4 + .../tangem/core/ui/test/BaseButtonTestTags.kt | 5 + .../ui/test/BuyTokenDetailsScreenTestTags.kt | 16 + .../core/ui/test/BuyTokenFiatListTestTags.kt | 6 + .../core/ui/test/BuyTokenScreenTestTags.kt | 6 + .../com/tangem/core/ui/test/DialogTestTags.kt | 1 - .../tangem/core/ui/test/MainScreenTestTags.kt | 1 + .../core/ui/test/NotificationTestTags.kt | 6 + .../test/ResidenceSettingsScreenTestTags.kt | 5 + .../test/SelectCountryBottomSheetTestTags.kt | 12 + .../SelectPaymentMethodBottomSheetTestTags.kt | 7 + .../test/SelectProviderBottomSheetTestTags.kt | 18 + .../tangem/core/ui/test/TopAppBarTestTags.kt | 7 + .../onramp/main/ui/OnrampAmountContent.kt | 15 +- .../onramp/main/ui/OnrampButtonComponent.kt | 3 + .../onramp/main/ui/OnrampProviderContent.kt | 6 + .../paymentmethod/ui/PaymentMethodIcon.kt | 5 +- .../ui/SelectPaymentMethodBottomSheet.kt | 9 +- .../providers/ui/SelectProviderBottomSheet.kt | 32 +- .../ui/SelectCountryBottomSheet.kt | 21 +- .../ui/SelectCurrencyBottomSheet.kt | 7 +- .../selecttoken/ui/OnrampSelectToken.kt | 5 +- .../settings/ui/OnrampSettingsContent.kt | 3 + .../onramp/tokenlist/ui/OnrampTokenList.kt | 8 +- .../multicurrency/MultiCurrencyAction.kt | 4 +- 40 files changed, 1214 insertions(+), 33 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt new file mode 100644 index 0000000000..eb4bc13f9e --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt @@ -0,0 +1,96 @@ +package com.tangem.common.utils + +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import java.io.IOException + +/** + * Method uses to set WireMock scenario state + */ +fun setWireMockScenarioState( + scenarioName: String, + state: String, + baseUrl: String = "[REDACTED_ENV_URL]" +): Boolean { + val client = OkHttpClient() + val json = """{"state": "$state"}""" + val mediaType = "application/json".toMediaType() + + val request = Request.Builder() + .url("$baseUrl/__admin/scenarios/$scenarioName/state") + .put(json.toRequestBody(mediaType)) + .build() + + return try { + client.newCall(request).execute().use { response -> + val body = response.body?.string() ?: "" + Timber.d("WireMock scenario request URL: ${request.url}") + Timber.d("WireMock scenario request body: $json") + Timber.d("WireMock scenario response: ${response.code} - ${response.message}") + Timber.d("WireMock scenario response body: $body") + response.isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "WireMock scenario error") + false + } +} + +/** + * Method checks accessibility of WireMock + */ +fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { + val client = OkHttpClient() + val request = Request.Builder() + .url("$baseUrl/__admin/scenarios") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + val body = response.body?.string() ?: "" + Timber.d("WireMock status check: ${response.code}") + Timber.d("Available scenarios: $body") + response.isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "WireMock not accessible") + false + } +} + +/** + * Method to reset all WireMock scenarios + */ +fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { + Timber.i("=== WireMock Scenarios Reset ===") + Timber.i("Base URL: $baseUrl") + + val client = OkHttpClient() + val url = "$baseUrl/__admin/scenarios/reset" + Timber.i("Request URL: $url") + + val request = Request.Builder() + .url(url) + .post("".toRequestBody()) + .build() + + return try { + Timber.d("Sending reset request...") + client.newCall(request).execute().use { response -> + Timber.d("Response code: ${response.code}") + Timber.d("Response message: ${response.message}") + val responseBody = response.body?.string() ?: "" + Timber.d("Response body: $responseBody") + + val isSuccessful = response.isSuccessful + Timber.d("Is successful: $isSuccessful") + isSuccessful + } + } catch (e: IOException) { + Timber.e(e, "Exception during reset") + false + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt new file mode 100644 index 0000000000..4193f3ab8b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenDetailsPageObject.kt @@ -0,0 +1,101 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags +import com.tangem.core.ui.test.NotificationTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag + +class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val topBarMoreButton: KNode = child { + hasTestTag(TopAppBarTestTags.MORE_BUTTON) + useUnmergedTree = true + } + + val topBarCloseButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val errorNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText(getResourceString(R.string.common_error)) + useUnmergedTree = true + } + + val errorNotificationText: KNode = child { + hasTestTag(NotificationTestTags.TEXT) + hasText(getResourceString(R.string.common_unknown_error)) + useUnmergedTree = true + } + + val refreshButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.warning_button_refresh)) + } + + val fiatCurrencyIcon: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON) + useUnmergedTree = true + } + + val expandFiatListButton: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON) + useUnmergedTree = true + } + + val fiatAmountTextField: KNode = child { + hasParent(withTestTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD)) + useUnmergedTree = true + } + + val tokenAmountField: KNode = child { + hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT)) + useUnmergedTree = true + } + + val providerLoadingTitle: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE) + } + + val providerLoadingText: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT) + } + + val providerTitle: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE) + useUnmergedTree = true + } + + val providerText: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT) + useUnmergedTree = true + } + + val buyButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + + val toSBlock: KNode = child { + hasTestTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onBuyTokenDetailsScreen(function: BuyTokenDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt new file mode 100644 index 0000000000..9e453330b3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenFiatListPageObject.kt @@ -0,0 +1,38 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.BuyTokenFiatListTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode + +class BuyTokenFiatListPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(BuyTokenFiatListTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + fun fiatListItemWithTitle(title: String): KNode { + return lazyList.child { + hasText(title) + } + } +} + +internal fun BaseTestCase.onBuyTokenFiatListBottomSheet(function: BuyTokenFiatListPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt new file mode 100644 index 0000000000..1dbe1d3ba3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/BuyTokenPageObject.kt @@ -0,0 +1,54 @@ +package com.tangem.screens + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.test.TokenElementsTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_buy)) + useUnmergedTree = true + } + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(BuyTokenScreenTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + @OptIn(ExperimentalTestApi::class) + fun tokenWithTitleAndFiatAmount(tokenTitle: String): KNode { + return lazyList.childWith { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasText(tokenTitle) + useUnmergedTree = true + }.child { + hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 960b371248..5bac035afc 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -3,6 +3,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.DialogTestTags import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen @@ -17,14 +18,19 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : } val cancelButton: KNode = child { - hasTestTag(DialogTestTags.BUTTON) + hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_cancel)) } val hideButton: KNode = child { - hasTestTag(DialogTestTags.BUTTON) + hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.token_details_hide_alert_hide)) } + + val confirmButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_confirm)) + } } internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index c9a08a06f8..ce3a3181b3 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -39,6 +39,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_generate_addresses)) } + val buyButton: KNode = child { + hasTestTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + /** * Find token list item with title and address */ diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt new file mode 100644 index 0000000000..c25bad4120 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ResidenceSettingsPageObject.kt @@ -0,0 +1,46 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.ResidenceSettingsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.onramp.impl.R as OnrampImplR + +class ResidenceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topBarTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.onramp_settings_title)) + useUnmergedTree = true + } + + val topBarCloseButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val residenceButton: KNode = child { + hasText(getResourceString(OnrampImplR.string.onramp_settings_residence)) + useUnmergedTree = true + } + + val countryName: KNode = child { + hasTestTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME) + useUnmergedTree = true + } + + val residenceSettingsDescription: KNode = child { + hasText(getResourceString(OnrampImplR.string.onramp_settings_residence_description)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onResidenceSettingsScreen(function: ResidenceSettingsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt new file mode 100644 index 0000000000..b3a6a9d573 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectCountryPageObject.kt @@ -0,0 +1,59 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText +import androidx.compose.ui.test.hasTestTag as withTestTag + + +class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(SelectCountryBottomSheetTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + val searchBar: KNode = child { + hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR) + useUnmergedTree = true + } + + fun countryWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasText(name) + hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON)) + useUnmergedTree = true + } + } + + fun unavailableCountryWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasText(name) + hasAnySibling(withText(getResourceString(R.string.onramp_country_unavailable))) + hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON)) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onSelectCountryBottomSheet(function: SelectCountryPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt new file mode 100644 index 0000000000..0739883572 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectPaymentMethodPageObject.kt @@ -0,0 +1,50 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsMatcher +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText + +class SelectPaymentMethodPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + + private val lazyList = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.onramp_pay_with)) + } + + fun paymentMethodWithNameAndIcon(name: String): KNode { + return lazyList.child { + hasAnyDescendant(withText(name)) + hasAnyDescendant(withTestTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON)) + useUnmergedTree = true + } + } +} + +internal fun BaseTestCase.onSelectPaymentMethodBottomSheet(function: SelectPaymentMethodPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt new file mode 100644 index 0000000000..2c8a050019 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectProviderPageObject.kt @@ -0,0 +1,91 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags +import com.tangem.features.onramp.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SelectProviderPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(R.string.onramp_choose_provider_title_hint)) + useUnmergedTree = true + } + + val paymentMethodIcon: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON) + useUnmergedTree = true + } + + val paymentMethodTitle: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME) + useUnmergedTree = true + } + + val paymentMethodName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME) + useUnmergedTree = true + } + + val paymentMethodExpandButton: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON) + useUnmergedTree = true + } + + val availableProviderItem: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM) + useUnmergedTree = true + } + + val availableProviderName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME) + useUnmergedTree = true + } + + val tokenAmount: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT) + useUnmergedTree = true + } + + val unavailableProviderItem: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM) + useUnmergedTree = true + } + + val unavailableProviderName: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME) + useUnmergedTree = true + } + + val moreProvidersIcon: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON) + useUnmergedTree = true + } + + val moreProvidersText: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT) + useUnmergedTree = true + } + + val bestRateLabel: KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL) + useUnmergedTree = true + } + + fun availableProviderWithName(name: String, tokenAmount: String, rate: String): KNode = child { + hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM) + hasAnyChild(withText(name)) + hasAnyChild(withText(tokenAmount)) + hasAnyChild(withText(rate)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSelectProviderBottomSheet(function: SelectProviderPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt new file mode 100644 index 0000000000..d65c64ed2b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -0,0 +1,469 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarios +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class BuyTokenTest : BaseTestCase() { + + @AllureId("3478") + @DisplayName("Onramp: error in providers loading") + @Test + fun errorInProvidersLoadingTest() { + setupHooks( + additionalAfterSection = { + resetWireMockScenarios() + } + ).run { + val tokenTitle = "Bitcoin" + val balance = "$184.85" + + resetWireMockScenarios() + + step("Setup WireMock scenario for 'Error' state") { + setWireMockScenarioState("payment_methods", "Error") + } + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Assert error notification title is displayed") { + onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() } + } + step("Assert error notification text is displayed") { + onBuyTokenDetailsScreen { errorNotificationText.assertIsDisplayed() } + } + step("Assert 'Refresh' button is displayed and clickable") { + onBuyTokenDetailsScreen { refreshButton.clickWithAssertion() } + } + } + } + + @AllureId("2565") + @DisplayName("Onramp: validate currency selector") + @Test + fun validateCurrencySelectorTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = "$184.85" + val popularFiatsTitle = "Popular Fiats" + val otherCurrenciesTitle = "Other currencies" + val australianDollar = "AUD" + val fiatAmount = "1" + val tokenAmount = "POL 488.24938338" + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider loading block' is displayed") { + onBuyTokenDetailsScreen { + providerLoadingTitle.assertIsDisplayed() + providerLoadingText.assertIsDisplayed() + } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { + tokenAmountField.assertTextContains(tokenAmount) + } + } + step("Fiat currency icon is displayed") { + onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() } + } + step("Click on 'Expand fiat list' button") { + onBuyTokenDetailsScreen { expandFiatListButton.clickWithAssertion() } + } + step("Assert '$popularFiatsTitle' is displayed") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(popularFiatsTitle).assertIsDisplayed() + } + } + step("Assert '$otherCurrenciesTitle' is displayed") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(otherCurrenciesTitle).assertIsDisplayed() + } + } + step("Click on fiat with title: '$australianDollar'") { + onBuyTokenFiatListBottomSheet { + fiatListItemWithTitle(australianDollar).performClick() + } + } + step("Assert new fiat currency: '$australianDollar' is displayed") { + onBuyTokenDetailsScreen { + fiatAmountTextField.assertTextContains(australianDollar + fiatAmount) + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { + tokenAmountField.assertTextContains(tokenAmount) + } + } + } + } + + @AllureId("2566") + @DisplayName("Onramp: validate 'Buy token' screen") + @Test + fun validateBuyTokenScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = "$184.85" + val euro = "EUR" + val fiatAmount = "1" + val tokenAmount = "POL 488.24938338" + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Assert 'Buy Token' title is displayed") { + onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") } + } + step("Assert 'More button' in top bar is displayed") { + onBuyTokenDetailsScreen { topBarMoreButton.assertIsDisplayed() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) } + } + step("Assert 'Provider loading block' is displayed") { + onBuyTokenDetailsScreen { + providerLoadingTitle.assertIsDisplayed() + providerLoadingText.assertIsDisplayed() + } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Assert token amount = '$tokenAmount'") { + onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) } + } + step("Assert 'ToS' block is displayed") { + onBuyTokenDetailsScreen { toSBlock.assertIsDisplayed()} + } + step("Assert 'Buy' button is displayed") { + onBuyTokenDetailsScreen { buyButton.assertIsDisplayed()} + } + step("Assert 'Close' button in top bar is displayed") { + onBuyTokenDetailsScreen { topBarCloseButton.assertIsDisplayed() } + } + } + } + + @AllureId("2563") + @DisplayName("Onramp: validate 'Residence' settings screen") + @Test + fun validateResidenceSettingsScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = "$184.85" + val country = "Albania" + val unavailableCountry = "Lebanon" + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Assert 'Buy $tokenTitle' title is displayed") { + onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") } + } + step("Click 'More' button in tab bar") { + onBuyTokenDetailsScreen { topBarMoreButton.clickWithAssertion() } + } + step("Assert 'Residence Settings' screen top bar title is displayed") { + onResidenceSettingsScreen { topBarTitle.assertIsDisplayed() } + } + step("Assert 'Residence Settings' screen top bar 'Close' button is displayed") { + onResidenceSettingsScreen { topBarCloseButton.assertIsDisplayed() } + } + step("Assert 'Residence' button is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { residenceButton.assertIsDisplayed() } + } + step("Assert country name is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { countryName.assertIsDisplayed() } + } + step("Assert residence settings description is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { residenceSettingsDescription.assertIsDisplayed() } + } + step("Click 'Residence button'") { + onResidenceSettingsScreen { residenceButton.clickWithAssertion() } + } + step("Assert 'Search bar' is displayed") { + onSelectCountryBottomSheet { searchBar.assertIsDisplayed() } + } + step("Type unavailable country name: '$unavailableCountry' in 'Search bar'") { + onSelectCountryBottomSheet { searchBar.performTextReplacement(unavailableCountry) } + } + step("Unavailable country: '$unavailableCountry' is displayed") { + onSelectCountryBottomSheet { unavailableCountryWithNameAndIcon(unavailableCountry).assertIsDisplayed() } + } + step("Type country name: '$country' in 'Search bar'") { + onSelectCountryBottomSheet { searchBar.performTextReplacement(country) } + } + step("Available country: '$country' is displayed") { + onSelectCountryBottomSheet { countryWithNameAndIcon(country).assertIsDisplayed() } + } + step("Click on country: '$country'") { + onSelectCountryBottomSheet { countryWithNameAndIcon(country).clickWithAssertion() } + } + step("Assert country: '$country' is displayed on 'Residence Settings' screen") { + onResidenceSettingsScreen { countryName.assertTextContains(country) } + } + } + } + + @AllureId("2570") + @DisplayName("Onramp: validate 'Select provider' bottom sheet") + @Test + fun validateProvidersScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = "$184.85" + val paymentMethod = "Card" + val fiatAmount = "1" + val providerNameMercuryo = "Mercuryo" + val providerNameSimplex = "Simplex" + val tokenAmount = "POL 488.24938338" + val bestRate = "Best rate" + val rate = "-0.00%" + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Open 'Select Provider' bottom sheet") { + onBuyTokenDetailsScreen { providerTitle.performClick() } + } + step("Assert available provider name is displayed") { + onSelectProviderBottomSheet { availableProviderItem.assertIsDisplayed() } + } + step("Assert unavailable provider name is displayed") { + onSelectProviderBottomSheet { unavailableProviderItem.assertIsDisplayed() } + } + step("Click on 'Expand payment methods' button") { + onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } + } + step("Click on payment method: '$paymentMethod'") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(paymentMethod).clickWithAssertion() } + } + step("Assert 'Select Provider' bottom sheet title is displayed") { + onSelectProviderBottomSheet { title.assertIsDisplayed() } + } + step("Assert payment method icon is displayed") { + onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() } + } + step("Assert payment method title is displayed") { + onSelectProviderBottomSheet { paymentMethodTitle.assertIsDisplayed() } + } + step("Assert payment method name is displayed") { + onSelectProviderBottomSheet { paymentMethodName.assertIsDisplayed() } + } + step("Assert provider with name: '$providerNameMercuryo' and rate: '$bestRate' is displayed") { + onSelectProviderBottomSheet { + availableProviderWithName(providerNameMercuryo, tokenAmount, bestRate).assertIsDisplayed() + } + } + step("Assert provider with name: '$providerNameSimplex' and rate: '$rate' is displayed") { + onSelectProviderBottomSheet { + availableProviderWithName(providerNameSimplex, tokenAmount, rate).assertIsDisplayed() + } + } + step("Assert 'More providers' icon is displayed") { + onSelectProviderBottomSheet { moreProvidersIcon.assertIsDisplayed() } + } + step("Assert 'More providers' text is displayed") { + onSelectProviderBottomSheet { moreProvidersText.assertIsDisplayed() } + } + step("Assert 'Best rate' label is displayed") { + onSelectProviderBottomSheet { bestRateLabel.assertIsDisplayed() } + } + } + } + + @AllureId("2570") + @DisplayName("Onramp: validate 'Select payment method' bottom sheet") + @Test + fun validatePaymentMethodScreenTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = "$184.85" + val card = "Card" + val googlePay = "Google Pay" + val invoiceRevolutPay = "Invoice Revolut Pay" + val sepa = "Sepa" + val fiatAmount = "1" + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on 'Buy' button") { + onMainScreen { buyButton.clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onBuyTokenScreen { + topAppBarTitle.assertIsDisplayed() + tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion() + } + } + step("Click on 'Confirm' button in 'Dialog'") { + onDialog { confirmButton.clickWithAssertion() } + } + step("Write fiat amount = '$fiatAmount'") { + onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) } + } + step("Assert 'Provider block' is displayed") { + onBuyTokenDetailsScreen { + providerTitle.assertIsDisplayed() + providerText.assertIsDisplayed() + } + } + step("Open 'Select Provider' bottom sheet") { + onBuyTokenDetailsScreen { providerTitle.performClick() } + } + step("Click on 'Expand payment methods' button") { + onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } + } + step("Assert 'Select Payment Method' bottom sheet title is displayed") { + onSelectPaymentMethodBottomSheet { title.assertIsDisplayed() } + } + step("Assert payment method: '$card' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(card).assertIsDisplayed() } + } + step("Assert payment method: '$googlePay' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(googlePay).assertIsDisplayed() } + } + step("Assert payment method: '$invoiceRevolutPay' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(invoiceRevolutPay).assertIsDisplayed() } + } + step("Assert payment method: '$sepa' is displayed") { + onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(sepa).assertIsDisplayed() } + } + step("Press 'Back' button") { + onSelectPaymentMethodBottomSheet { device.uiDevice.pressBack() } + } + step("Assert 'Select Provider' bottom sheet title is displayed") { + onSelectProviderBottomSheet { title.assertIsDisplayed() } + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt index dd0dee7e9e..6bbf318ec7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt @@ -17,7 +17,7 @@ class HideTokenTest : BaseTestCase() { @Test fun hideWalletTokenByHideButtonTest() { val tokenTitle = "Polygon" - val balance = "<$0.01" + val balance = "$184.85" setupHooks().run { step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt index 716d79978f..319beeb81f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TopAppBarTestTags /** * [TangemTopAppBar] height options. @@ -127,6 +129,7 @@ fun TangemTopAppBar( TopAppBarButton( button = endButton, tint = iconTint, + modifier = modifier.testTag(TopAppBarTestTags.MORE_BUTTON), ) } }, @@ -172,6 +175,7 @@ fun TangemTopAppBar( TopAppBarButton( button = startButton, tint = iconTint, + modifier = modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON), ) } } @@ -222,6 +226,7 @@ private fun TopAppBarTitle( color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(TopAppBarTestTags.TITLE), ) AnimatedVisibility( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 3a2dfe9d3d..90dbd2db78 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.DialogTestTags +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.utils.MultipleClickPreventer @Suppress("LongParameterList") @@ -51,7 +51,7 @@ fun TangemButton( Button( modifier = modifier .heightIn(min = size.toHeightDp()) - .testTag(DialogTestTags.BUTTON), + .testTag(BaseButtonTestTags.BUTTON), onClick = { multipleClickPreventer.processEvent { if (!showProgress) onClick() } }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index a302980668..4366b157be 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags @Composable fun SearchBar( @@ -57,7 +59,8 @@ fun SearchBar( } else { state.onActiveChange(false) } - }, + } + .testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR), enabled = enabled, value = state.query, onValueChange = state.onQueryChange, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 10bfdbba1d..1e8c2f342d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -20,6 +20,7 @@ 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.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.NotificationTestTags import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** @@ -206,6 +208,7 @@ internal fun TextsBlock( text = titleText, color = titleColor, style = TangemTheme.typography.button, + modifier = modifier.testTag(NotificationTestTags.TITLE), ) SpacerH(height = TangemTheme.dimens.spacing2) @@ -215,6 +218,7 @@ internal fun TextsBlock( text = subtitle.resolveReference(), color = subtitleColor, style = TangemTheme.typography.caption2, + modifier = modifier.testTag(NotificationTestTags.TEXT), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt new file mode 100644 index 0000000000..9d4954ba04 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object BaseButtonTestTags { + const val BUTTON = "BASE_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt new file mode 100644 index 0000000000..0f045c5da5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenDetailsScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object BuyTokenDetailsScreenTestTags { + const val EXPAND_FIAT_LIST_BUTTON = "BUY_TOKEN_DETAILS_SCREEN_EXPAND_FIAT_LIST_BUTTON" + const val FIAT_CURRENCY_ICON = "BUY_TOKEN_DETAILS_SCREEN_FIAT_CURRENCY_ICON" + const val FIAT_AMOUNT_TEXT_FIELD = "BUY_TOKEN_DETAILS_SCREEN_FIAT_AMOUNT_TEXT_FIELD" + const val TOKEN_AMOUNT = "BUY_TOKEN_DETAILS_SCREEN_TOKEN_AMOUNT" + + const val PROVIDER_LOADING_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE" + const val PROVIDER_LOADING_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE" + + const val PROVIDER_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TITLE" + const val PROVIDER_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TEXT" + + const val TOS_BLOCK = "BUY_TOKEN_DETAILS_SCREEN_TOS_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt new file mode 100644 index 0000000000..a656caf7f0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenFiatListTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object BuyTokenFiatListTestTags { + const val LAZY_LIST = "BUY_TOKEN_FIAT_LIST_LAZY_LIST" + const val LAZY_LIST_ITEM = "BUY_TOKEN_FIAT_LIST_LAZY_LIST_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt new file mode 100644 index 0000000000..af41f67814 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BuyTokenScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object BuyTokenScreenTestTags { + const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST" + const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt index 8f911d8632..0c5eae6e75 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DialogTestTags.kt @@ -2,5 +2,4 @@ package com.tangem.core.ui.test object DialogTestTags { const val DIALOG_CONTAINER = "DIALOG_CONTAINER" - const val BUTTON = "DIALOG_BUTTON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt index f635ce4544..dfef1326a7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt @@ -8,4 +8,5 @@ object MainScreenTestTags { const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE" const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM" const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON" + const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt new file mode 100644 index 0000000000..15a63e2186 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/NotificationTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object NotificationTestTags { + const val TITLE = "NOTIFICATION_TITLE" + const val TEXT = "NOTIFICATION_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt new file mode 100644 index 0000000000..9ebf2b26e0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ResidenceSettingsScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object ResidenceSettingsScreenTestTags { + const val COUNTRY_NAME = "RESIDENCE_SETTINGS_SCREEN_COUNTRY_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt new file mode 100644 index 0000000000..6a2d859e19 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectCountryBottomSheetTestTags.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.test + +object SelectCountryBottomSheetTestTags { + + const val LAZY_LIST = "SELECT_COUNTRY_BOTTOM_SHEET_LAZY_LIST" + const val COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ITEM" + const val UNAVAILABLE_COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ITEM" + const val SEARCH_BAR = "SELECT_COUNTRY_BOTTOM_SHEET_SEARCH_BAR" + const val COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ICON" + const val UNAVAILABLE_COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ICON" + const val COUNTRY_NAME = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt new file mode 100644 index 0000000000..ebb601e9a5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectPaymentMethodBottomSheetTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object SelectPaymentMethodBottomSheetTestTags { + + const val LAZY_LIST = "SELECT_PAYMENT_METHOD_LAZY_LIST" + const val PAYMENT_METHOD_ICON = "PAYMENT_METHOD_NAME" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt new file mode 100644 index 0000000000..d457d18189 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectProviderBottomSheetTestTags.kt @@ -0,0 +1,18 @@ +package com.tangem.core.ui.test + +object SelectProviderBottomSheetTestTags { + + const val PAYMENT_METHOD_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_ICON" + const val PAYMENT_METHOD_TITLE = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_TITLE" + const val PAYMENT_METHOD_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_NAME" + const val PAYMENT_METHOD_EXPAND_BUTTON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_EXPAND_BUTTON" + const val TOKEN_AMOUNT = "SELECT_PROVIDER_BOTTOM_SHEET_TOKEN_AMOUNT" + const val AVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_NAME" + const val AVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_ITEM" + const val UNAVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_ITEM" + const val UNAVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_NAME" + const val UNAVAILABLE_PROVIDER_SUBTITLE = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_SUBTITLE" + const val MORE_PROVIDERS_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_ICON" + const val MORE_PROVIDERS_TEXT = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_TEXT" + const val BEST_RATE_LABEL = "SELECT_PROVIDER_BOTTOM_SHEET_BEST_RATE_LABEL" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt new file mode 100644 index 0000000000..1c2c1b09bf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TopAppBarTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object TopAppBarTestTags { + const val TITLE = "TOP_APP_BAR_TITLE" + const val MORE_BUTTON = "TOP_APP_BAR_MORE_BUTTON" + const val CLOSE_BUTTON = "TOP_APP_BAR_CLOSE_BUTTON" +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 07d5b17e3c..a493e1a7c9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection @@ -26,6 +27,7 @@ import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM @@ -84,7 +86,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) - .requiredHeightIn(min = TangemTheme.dimens.size32), + .requiredHeightIn(min = TangemTheme.dimens.size32) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), ) LaunchedEffect(key1 = Unit) { @@ -101,7 +104,8 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { top = TangemTheme.dimens.spacing8, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - ), + ) + .testTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT), contentAlignment = Alignment.Center, ) { when (state) { @@ -138,12 +142,15 @@ private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size40) - .clip(CircleShape), + .clip(CircleShape) + .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), model = currencyUM.iconUrl, contentDescription = null, ) Icon( - modifier = Modifier.size(TangemTheme.dimens.size16), + modifier = Modifier + .size(TangemTheme.dimens.size16) + .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), painter = painterResource(id = R.drawable.ic_chevron_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt index d63b3aa736..fe7e8c0075 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -15,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampMainComponentUM import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM @@ -77,6 +79,7 @@ private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ), + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK), onClick = { offset -> clickableAnnotation.getStringAnnotations( tag = TERMS_OF_USE_KEY, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index e13fd96310..8d2bf768e6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -13,12 +13,14 @@ 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.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import com.tangem.core.ui.extensions.appendSpace import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon @@ -60,6 +62,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE), ) Text( text = buildAnnotatedString { @@ -69,6 +72,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT), ) } AnimatedVisibility( @@ -108,6 +112,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { text = stringResourceSafe(id = R.string.express_provider), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE), ) Row( verticalAlignment = Alignment.CenterVertically, @@ -122,6 +127,7 @@ private fun OnrampProviderLoading(modifier: Modifier = Modifier) { text = stringResourceSafe(id = R.string.express_fetch_best_rates), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT), ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt index 18e7c56005..8c6125e33d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/PaymentMethodIcon.kt @@ -7,10 +7,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags @Composable internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier) { @@ -19,7 +21,8 @@ internal fun PaymentMethodIcon(imageUrl: String, modifier: Modifier = Modifier) .size(TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8) .background(TangemColorPalette.Light1) - .padding(TangemTheme.dimens.spacing6), + .padding(TangemTheme.dimens.spacing6) + .testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON), model = ImageRequest.Builder(context = LocalContext.current) .data(imageUrl) .crossfade(enable = true) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index fb80254c3c..ee929788ee 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -11,11 +11,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig @@ -49,7 +51,7 @@ private fun SelectPaymentMethodBottomSheetContent( modifier: Modifier = Modifier, ) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST), ) { items( items = methods, @@ -86,7 +88,10 @@ private fun PaymentMethodItem(paymentMethod: PaymentMethodUM, isSelected: Boolea verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - PaymentMethodIcon(paymentMethod.imageUrl) + PaymentMethodIcon( + imageUrl = paymentMethod.imageUrl, + modifier = Modifier.testTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON), + ) Text( text = paymentMethod.name, style = TangemTheme.typography.subtitle2, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 59cb8cb0b0..116ca08de5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign @@ -32,6 +33,7 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon @@ -101,15 +103,19 @@ private fun PaymentMethodBlock( text = stringResourceSafe(id = R.string.onramp_pay_with), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_TITLE), ) Text( text = state.name, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME), ) } Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), + modifier = Modifier + .size(TangemTheme.dimens.size24) + .testTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON), painter = painterResource(id = R.drawable.ic_chevron_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -149,14 +155,16 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m modifier = modifier .selectedBorder(isSelected = state.isSelected) .clickable(onClick = state.onClick) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { SubcomposeAsyncImage( modifier = Modifier .size(size = TangemTheme.dimens.size40) - .clip(TangemTheme.shapes.roundedCorners8), + .clip(TangemTheme.shapes.roundedCorners8) + .testTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME), model = ImageRequest.Builder(context = LocalContext.current) .data(state.imageUrl) .crossfade(enable = true) @@ -166,7 +174,9 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m contentDescription = null, ) Text( - modifier = Modifier.weight(1F), + modifier = Modifier + .weight(1F) + .testTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT), text = state.name, style = TangemTheme.typography.subtitle2, color = if (state.isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, @@ -189,7 +199,8 @@ private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, m modifier = Modifier .clip(RoundedCornerShape(4.dp)) .background(TangemTheme.colors.icon.accent) - .padding(vertical = 1.dp, horizontal = 6.dp), + .padding(vertical = 1.dp, horizontal = 6.dp) + .testTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL), ) } state.diffRate != null -> { @@ -218,7 +229,8 @@ private fun UnavailableProviderItem( Row( modifier = modifier .selectedBorder(isSelected = isSelected) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { @@ -240,11 +252,13 @@ private fun UnavailableProviderItem( text = providerName, style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.secondary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME), ) Text( text = subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_SUBTITLE), ) } } @@ -259,7 +273,8 @@ private fun OnrampMoreProviders() { contentDescription = null, tint = TangemTheme.colors.icon.informative, modifier = Modifier - .padding(top = 16.dp), + .padding(top = 16.dp) + .testTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON), ) Text( text = stringResourceSafe(R.string.express_more_providers_soon), @@ -267,7 +282,8 @@ private fun OnrampMoreProviders() { color = TangemTheme.colors.icon.informative, modifier = Modifier .padding(top = 4.dp, bottom = 24.dp) - .padding(horizontal = TangemTheme.dimens.spacing56), + .padding(horizontal = TangemTheme.dimens.spacing56) + .testTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT), textAlign = TextAlign.Center, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index aaea4ccb7e..741e13aaa6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage @@ -25,6 +26,7 @@ import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcountry.entity.CountryItemState import com.tangem.features.onramp.selectcountry.entity.CountryListUM @@ -40,7 +42,7 @@ internal fun SelectCountryBottomSheet(config: TangemBottomSheetConfig, content: @Composable internal fun OnrampCountryList(state: CountryListUM, modifier: Modifier = Modifier) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(SelectCountryBottomSheetTestTags.LAZY_LIST), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) { item(key = "search_bar") { @@ -102,13 +104,15 @@ private fun LazyListScope.countryListWithContent(state: CountryListUM.Content) { modifier = Modifier .fillMaxWidth() .clickable(onClick = item.onClick) - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_ITEM), state = item, ) is CountryItemState.WithContent.Unavailable -> UnavailableCountryItem( modifier = Modifier .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ITEM), state = item, ) } @@ -138,13 +142,17 @@ private fun ContentCountryItem(state: CountryItemState.WithContent.Content, modi verticalAlignment = Alignment.CenterVertically, ) { AsyncImage( - modifier = Modifier.size(TangemTheme.dimens.size36), + modifier = Modifier + .size(TangemTheme.dimens.size36) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON), model = state.flagUrl, contentDescription = null, ) Text( text = state.countryName, - modifier = Modifier.weight(1F), + modifier = Modifier + .weight(1F) + .testTag(SelectCountryBottomSheetTestTags.COUNTRY_NAME), overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.subtitle2, @@ -172,7 +180,8 @@ private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailab AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size36) - .alpha(0.4F), + .alpha(0.4F) + .testTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON), model = state.flagUrl, contentDescription = null, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt index 8bcf48db89..cbc2cb8075 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer @@ -25,6 +26,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenFiatListTestTags import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState @@ -40,7 +42,7 @@ internal fun SelectCurrencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable internal fun OnrampCurrencyList(state: CurrenciesListUM, modifier: Modifier = Modifier) { LazyColumn( - modifier = modifier, + modifier = modifier.testTag(BuyTokenFiatListTestTags.LAZY_LIST), contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), ) { item(key = "search_bar") { @@ -106,7 +108,8 @@ private fun LazyListScope.currencyListContent(state: CurrenciesListUM.Content) { modifier = Modifier .fillMaxWidth() .clickable(onClick = item.onClick) - .padding(vertical = TangemTheme.dimens.spacing16), + .padding(vertical = TangemTheme.dimens.spacing16) + .testTag(BuyTokenFiatListTestTags.LAZY_LIST_ITEM), currency = item, ) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index d2c298c8f6..689cee2068 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -10,10 +10,12 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.impl.R @@ -36,7 +38,8 @@ internal fun OnrampSelectToken( .nestedScroll(nestedScrollConnection) .background(TangemTheme.colors.background.secondary) .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(BuyTokenScreenTestTags.LAZY_LIST), ) { stickyHeader(key = "header") { AppBarWithBackButton( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt index 8a0d062131..33ba297a0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/ui/OnrampSettingsContent.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage @@ -19,6 +20,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.ResidenceSettingsScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.settings.entity.OnrampSettingsItemUM @@ -95,6 +97,7 @@ private fun ResidenceSection(state: OnrampSettingsItemUM.Residence, modifier: Mo text = state.countryName, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME), ) Icon( painter = painterResource(id = R.drawable.ic_chevron_right_24), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index c45da297d2..27943a21c4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -24,6 +26,8 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList @@ -90,7 +94,9 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: lastIndex = items.lastIndex, addDefaultPadding = false, backgroundColor = TangemTheme.colors.background.primary, - ), + ) + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .semantics { lazyListItemPosition = index }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt index e82c67b4fe..8465d3fca3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAction.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp @@ -14,6 +15,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.ActionButtonContent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.MainScreenTestTags /** [REDACTED_AUTHOR] @@ -48,7 +50,7 @@ internal fun MultiCurrencyAction( paddingBetweenIconAndText = 4.dp, ) }, - modifier = modifier, + modifier = modifier.testTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON), color = TangemTheme.colors.button.secondary, ) } \ No newline at end of file From b2a199a56800b1873ab395c1f496a0a8c221ed38 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Jul 2025 16:16:39 +0400 Subject: [PATCH 005/165] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 6 + .../tap/di/domain/TokensDomainModule.kt | 5 + .../staking/di/YieldBalanceSupplierModule.kt | 5 +- .../DefaultSingleYieldBalanceProducer.kt | 42 +--- .../DefaultSingleYieldBalanceProducerTest.kt | 207 ++++++++---------- .../tangem/domain/staking/StakingIdFactory.kt | 27 ++- .../single/SingleYieldBalanceProducer.kt | 11 +- domain/tokens/build.gradle.kts | 7 +- .../BaseCurrencyStatusOperations.kt | 34 ++- .../CachedCurrenciesStatusesOperations.kt | 20 +- 10 files changed, 177 insertions(+), 187 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 213e3753c7..7f6857ddf0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -210,4 +210,10 @@ internal object StakingDomainModule { fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase { return GetActionRequirementAmountUseCase() } + + @Provides + @Singleton + fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory { + return StakingIdFactory(walletManagersFacade = walletManagersFacade) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index eddbbc0fd4..b1d8469ec4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -11,6 +11,7 @@ import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -405,6 +406,7 @@ internal object TokensDomainModule { singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + stakingIdFactory: StakingIdFactory, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -420,6 +422,7 @@ internal object TokensDomainModule { multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, ) } @@ -439,6 +442,7 @@ internal object TokensDomainModule { singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -454,6 +458,7 @@ internal object TokensDomainModule { multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt index 3407a3d99b..3ef90f5b74 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt @@ -42,9 +42,8 @@ internal object YieldBalanceSupplierModule { listOf( "single_yield_balance", params.userWalletId.stringValue, - params.currencyId.value, - params.network.id.rawId, - params.network.id.derivationPath, + params.stakingId.integrationId, + params.stakingId.address, ) .joinToString(separator = "_") }, diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index bd38f407a4..cac9b3c1a3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -2,8 +2,6 @@ package com.tangem.data.staking.single import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.data.staking.utils.StakingIdFactory -import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier @@ -24,7 +22,7 @@ import timber.log.Timber * * @property params params * @property multiYieldBalanceSupplier multi yield balance supplier - * @property stakingIdFactory factory for creating [StakingID] + * @property analyticsExceptionHandler analytics exception handler * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -32,20 +30,17 @@ import timber.log.Timber internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( @Assisted private val params: SingleYieldBalanceProducer.Params, private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - private val stakingIdFactory: StakingIdFactory, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, ) : SingleYieldBalanceProducer { override val fallback: YieldBalance by lazy { YieldBalance.Error( - integrationId = stakingIdFactory.createIntegrationId(currencyId = params.currencyId), - address = null, + integrationId = params.stakingId.integrationId, + address = params.stakingId.address, ) } - private var stakingId: StakingID? = null - override fun produce(): Flow { Timber.i("Producing yield balance for params:\n$params") @@ -53,12 +48,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> - val currentStakingId = getStakingId() - - if (currentStakingId == null) { - Timber.i("Staking ID is null for params: $params") - return@mapNotNull YieldBalance.Unsupported - } + val currentStakingId = params.stakingId val currentBalances = balances.filter { it.getStakingId() == currentStakingId } @@ -86,34 +76,16 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( currentBalances.first() } } else { - val balance = currentBalances.firstOrNull() + val balance = currentBalances.firstOrNull() ?: return@mapNotNull null - if (balance != null) { - Timber.i("Yield balance found for $currentStakingId:\n$balance") - balance - } else { - Timber.i("No yield balance found for $currentStakingId:\n${YieldBalance.Unsupported}") - YieldBalance.Unsupported - } + Timber.i("Yield balance found for $currentStakingId:\n$balance") + balance } } .distinctUntilChanged() .flowOn(dispatchers.default) } - private suspend fun getStakingId(): StakingID? { - val saved = stakingId - - if (saved != null) return saved - - return stakingIdFactory.create( - userWalletId = params.userWalletId, - currencyId = params.currencyId, - network = params.network, - ) - .also { stakingId = it } - } - @AssistedFactory interface Factory : SingleYieldBalanceProducer.Factory { override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index 1d7e5af029..b6f14b7e6b 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -1,13 +1,10 @@ package com.tangem.data.staking.single import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.getEmittedValues import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.data.staking.toDomain -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -15,40 +12,49 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.flow.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleYieldBalanceProducerTest { private val params = SingleYieldBalanceProducer.Params( userWalletId = UserWalletId(stringValue = "011"), - currencyId = ton.id, - network = ton.network, + stakingId = tonId, ) private val multiNetworkStatusSupplier = mockk() - private val stakingIdFactory = mockk() private val analyticsExceptionHandler = mockk(relaxUnitFun = true) private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultSingleYieldBalanceProducer( params = params, - stakingIdFactory = stakingIdFactory, multiYieldBalanceSupplier = multiNetworkStatusSupplier, analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = dispatchers, ) + @BeforeEach + fun resetMocks() { + clearMocks(multiNetworkStatusSupplier, analyticsExceptionHandler) + } + @Test - fun `test that flow is mapped for data from params`() = runTest { + fun `flow is mapped for data from params`() = runTest { + // Arrange val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - val expected = flowOf( + val multiFlow = flowOf( setOf( balance, MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), @@ -56,97 +62,89 @@ internal class DefaultSingleYieldBalanceProducerTest { ) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produce() + // Act + val actual = getEmittedValues(flow = producer.produce()) - verify { multiNetworkStatusSupplier(multiParams) } + Truth.assertThat(actual).hasSize(1) + Truth.assertThat(actual).containsExactly(balance) - val values = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test that flow is updated if yield balance is updated`() = runTest { - val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + fun `flow is updated if yield balance is updated`() = runTest { + // Arrange + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } - - // first emit val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - expected.emit(value = setOf(balance)) + val updatedBalance = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address) - val values1 = getEmittedValues(flow = actual) + // Act (first emit) + multiFlow.emit(value = setOf(balance)) + val actual1 = getEmittedValues(flow = producerFlow) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + // Assert (first emit) + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(balance) - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(balance)) + // Act (second emit) + multiFlow.emit(value = setOf(updatedBalance)) + val actual2 = getEmittedValues(flow = producerFlow) - // second emit - val updatedStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address) - expected.emit(value = setOf(updatedStatus)) + // Assert (second emit) + Truth.assertThat(actual2).hasSize(2) + Truth.assertThat(actual2).containsExactly(balance, updatedBalance) - val values2 = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(2) - Truth.assertThat(values2).isEqualTo(listOf(balance, updatedStatus)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test that flow is filtered the same status`() = runTest { - val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + fun `flow is filtered the same status`() = runTest { + // Arrange + val multiFlow = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } - - // first emit val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - expected.emit(value = setOf(balance)) - val values1 = getEmittedValues(flow = actual) + // Act (first emit) + multiFlow.emit(value = setOf(balance)) + val actual1 = getEmittedValues(flow = producerFlow) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + // Assert (first emit) + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(balance) - Truth.assertThat(values1.size).isEqualTo(1) - Truth.assertThat(values1).isEqualTo(listOf(balance)) + // Act (second emit) + multiFlow.emit(value = setOf(balance)) + val actual2 = getEmittedValues(flow = producerFlow) - // second emit - expected.emit(value = setOf(balance)) + // Assert (second emit) + Truth.assertThat(actual2).hasSize(1) + Truth.assertThat(actual2).containsExactly(balance) - val values2 = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test if flow throws exception`() = runTest { + fun `flow throws exception`() = runTest { + // Arrange val exception = IllegalStateException() + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() val innerFlow = MutableStateFlow(value = false) - val expected = flow { + val multiFlow = flow { if (innerFlow.value) { emit(setOf(balance)) } else { @@ -156,83 +154,52 @@ internal class DefaultSingleYieldBalanceProducerTest { .buffer(capacity = 5) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns expected - every { stakingIdFactory.createIntegrationId(currencyId = params.currencyId) } returns tonId.integrationId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produceWithFallback() + val producerFlow = producer.produceWithFallback() - verify { multiNetworkStatusSupplier(multiParams) } + // Act (first emit) + val actual1 = getEmittedValues(flow = producerFlow) - val values1 = getEmittedValues(flow = actual) + // Assert (first emit) + val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = "0x1") - coVerify(inverse = true) { stakingIdFactory.create(any(), any(), any()) } - - Truth.assertThat(values1.size).isEqualTo(1) - val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null) - Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) - - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + Truth.assertThat(actual1).hasSize(1) + Truth.assertThat(actual1).containsExactly(fallbackStatus) + // Act (second emit) innerFlow.emit(value = true) + val actual2 = getEmittedValues(flow = producerFlow) - val values2 = getEmittedValues(flow = actual) + Truth.assertThat(actual2).hasSize(1) + Truth.assertThat(actual2).containsExactly(balance) - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - Truth.assertThat(values2.size).isEqualTo(1) - Truth.assertThat(values2).isEqualTo(listOf(balance)) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } @Test - fun `test if flow doesn't contain network from params`() = runTest { + fun `flow doesn't contain network from params`() = runTest { + // Arrange val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain() - val yieldBalancesFlow = flowOf(setOf(balance)) + val multiFlow = flowOf(setOf(balance)) val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns tonId + every { multiNetworkStatusSupplier(multiParams) } returns multiFlow - val actual = producer.produce() + val producerFlow = producer.produce() - verify { multiNetworkStatusSupplier(multiParams) } + // Act + val actual = getEmittedValues(flow = producerFlow) - val values = getEmittedValues(flow = actual) + // Assert + Truth.assertThat(actual).isEmpty() - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - val expected = YieldBalance.Unsupported - Truth.assertThat(values.first()).isEqualTo(expected) - } - - @Test - fun `test if wallet manager facade returns null`() = runTest { - val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - - val yieldBalancesFlow = flowOf(setOf(balance)) - - val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) - every { multiNetworkStatusSupplier(multiParams) } returns yieldBalancesFlow - coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns null - - val actual = producer.produce() - - verify { multiNetworkStatusSupplier(multiParams) } - - val values = getEmittedValues(flow = actual) - - coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } - - val expected = YieldBalance.Unsupported - Truth.assertThat(values.first()).isEqualTo(expected) + verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } private companion object { - val mocks = MockCryptoCurrencyFactory() - - val ton = mocks.createCoin(Blockchain.TON) - val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt index 0da69ebb56..a830af4484 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt @@ -42,12 +42,37 @@ class StakingIdFactory( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network, + ): Either { + return createInternal( + currencyId = currencyId, + defaultAddressProvider = { + walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + }, + ) + } + + /** + * Creates a [StakingID] for the given cryptocurrency and default address + * + * @param currencyId the identifier of the cryptocurrency + * @param defaultAddress the default address for staking, can be null + */ + fun create(currencyId: CryptoCurrency.ID, defaultAddress: String?): Either { + return createInternal( + currencyId = currencyId, + defaultAddressProvider = { defaultAddress }, + ) + } + + private inline fun createInternal( + currencyId: CryptoCurrency.ID, + defaultAddressProvider: () -> String?, ): Either = either { val integrationId = StakingIntegrationID.create(currencyId = currencyId) ensureNotNull(integrationId) { Error.UnsupportedCurrency } - val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() } ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt index aae567c354..7fea7e7014 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt @@ -1,10 +1,9 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.staking.model.stakekit.YieldBalance /** * Producer of yield balance for selected wallet [UserWalletId] @@ -15,16 +14,14 @@ interface SingleYieldBalanceProducer : FlowProducer { data class Params( val userWalletId: UserWalletId, - val currencyId: CryptoCurrency.ID, - val network: Network, + val stakingId: StakingID, ) { override fun toString(): String { return """ SingleYieldBalanceProducer.Params( userWalletId = $userWalletId, - currencyId = $currencyId, - network = $network + stakingId = $stakingId, ) """.trimIndent() } diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 2bcbd75a88..a1d0b5b08f 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -54,6 +54,10 @@ dependencies { implementation(deps.jodatime) implementation(deps.reKotlin) + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + /** Tests */ testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) @@ -61,7 +65,4 @@ dependencies { testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) - testImplementation(tangemDeps.blockchain) { - exclude(module = "joda-time") - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 351c2e0f89..d3c6c507a6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.recover +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -18,6 +19,9 @@ import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.staking.model.isStakingSupported import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceProducer @@ -49,6 +53,7 @@ abstract class BaseCurrencyStatusOperations( private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -82,7 +87,7 @@ abstract class BaseCurrencyStatusOperations( return getCurrencyStatusFlow(userWalletId = userWalletId, currency = currency) } - fun getCurrencyStatusFlow( + suspend fun getCurrencyStatusFlow( userWalletId: UserWalletId, currency: CryptoCurrency, includeQuotes: Boolean = true, @@ -105,9 +110,24 @@ abstract class BaseCurrencyStatusOperations( val statusFlow = getNetworkStatus(userWalletId = userWalletId, network = currency.network) - val yieldBalanceFlow = getYieldBalance(userWalletId = userWalletId, cryptoCurrency = currency) + val isStakingSupported = currency.network.toBlockchain().isStakingSupported - return if (subscribeOnYieldBalance) { + val yieldBalanceFlow = if (isStakingSupported) { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = currency.id, + network = currency.network, + ) + .getOrNull() + + stakingId?.let { + getYieldBalance(userWalletId = userWalletId, stakingId = it) + } + } else { + null + } + + return if (subscribeOnYieldBalance && yieldBalanceFlow != null) { combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, @@ -334,15 +354,11 @@ abstract class BaseCurrencyStatusOperations( .bind() } - private fun getYieldBalance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): EitherFlow { + private fun getYieldBalance(userWalletId: UserWalletId, stakingId: StakingID): EitherFlow { return singleYieldBalanceSupplier( params = SingleYieldBalanceProducer.Params( userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, + stakingId = stakingId, ), ) .map> { it.right() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 6bcb07b46b..5c5c5d91b2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -24,6 +24,7 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -56,6 +57,7 @@ class CachedCurrenciesStatusesOperations( private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( @@ -67,6 +69,7 @@ class CachedCurrenciesStatusesOperations( singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, tokensFeatureToggles = tokensFeatureToggles, ) { @@ -371,20 +374,19 @@ class CachedCurrenciesStatusesOperations( return channelFlow { val state = MutableStateFlow(emptyList()) - cryptoCurrencies.onEach { + val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) + .getOrNull() + } + + stakingIds.onEach { launch { singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params( - userWalletId = userWalletId, - currencyId = it.id, - network = it.network, - ), + params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = it), ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { - it.integrationId == balance.integrationId && it.address == balance.address - } + loadedBalances.addOrReplace(balance) { balance.getStakingId() == it } } } .launchIn(scope = this) From 3f60d158200590e0dbbec5c78f4117cf02bc53d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Jul 2025 18:58:54 +0400 Subject: [PATCH 006/165] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 3 + .../tap/di/domain/MarketsDomainModule.kt | 3 + .../tap/di/domain/StakingDomainModule.kt | 4 +- .../tap/di/domain/TokensDomainModule.kt | 10 + .../domain/token/MockCryptoCurrencyFactory.kt | 1 + .../com/tangem/common/test/utils/TruthExt.kt | 19 +- .../multi/DefaultMultiYieldBalanceFetcher.kt | 39 +--- .../DefaultSingleYieldBalanceFetcher.kt | 4 +- .../DefaultMultiYieldBalanceFetcherTest.kt | 192 ++++-------------- .../DefaultSingleYieldBalanceFetcherTest.kt | 24 +-- .../managetokens/SaveManagedTokensUseCase.kt | 11 +- .../domain/markets/SaveMarketTokensUseCase.kt | 11 +- .../FetchStakingYieldBalanceUseCase.kt | 45 ++-- .../staking/multi/MultiYieldBalanceFetcher.kt | 15 +- .../single/SingleYieldBalanceFetcher.kt | 9 +- .../tokens/AddCryptoCurrenciesUseCase.kt | 32 ++- .../tokens/FetchCardTokenListUseCase.kt | 13 +- .../tokens/FetchCurrencyStatusUseCase.kt | 30 ++- .../domain/tokens/FetchTokenListUseCase.kt | 13 +- .../CachedCurrenciesStatusesOperations.kt | 6 +- .../tokens/wallet/WalletBalanceFetcher.kt | 35 +++- .../tokens/wallet/WalletBalanceFetcherTest.kt | 174 +++++++++++++++- .../intents/WalletWarningsClickIntents.kt | 15 +- 23 files changed, 405 insertions(+), 303 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 55d82f0af5..0db474821f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -6,6 +6,7 @@ import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -72,6 +73,7 @@ internal object ManageTokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( customTokensRepository = customTokensRepository, @@ -81,6 +83,7 @@ internal object ManageTokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 1dab6fc6f5..71f6985831 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -63,6 +64,7 @@ object MarketsDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, @@ -71,6 +73,7 @@ object MarketsDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 7f6857ddf0..f98c7d2bd2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -94,12 +94,12 @@ internal object StakingDomainModule { @Provides @Singleton fun provideFetchStakingYieldBalanceUseCase( - stakingErrorResolver: StakingErrorResolver, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchStakingYieldBalanceUseCase { return FetchStakingYieldBalanceUseCase( - stakingErrorResolver = stakingErrorResolver, singleYieldBalanceFetcher = singleYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index b1d8469ec4..70d26f85a2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -47,6 +47,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, @@ -55,6 +56,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -65,12 +67,14 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -173,6 +177,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher: SingleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, tokensFeatureToggles: TokensFeatureToggles, + stakingIdFactory: StakingIdFactory, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, @@ -181,6 +186,7 @@ internal object TokensDomainModule { singleYieldBalanceFetcher = singleYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, + stakingIdFactory = stakingIdFactory, ) } @@ -191,12 +197,14 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( currenciesRepository = currenciesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, ) } @@ -477,6 +485,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { return WalletBalanceFetcher( @@ -486,6 +495,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) } diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index 6592071c92..efba92dc00 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -25,6 +25,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) } val chia by lazy { createCoin(Blockchain.Chia) } val ethereum by lazy { createCoin(Blockchain.Ethereum) } + val stellar by lazy { createCoin(Blockchain.Stellar) } val chiaAndEthereum by lazy { listOf( diff --git a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt index c8a88ac0ff..67cd37711b 100644 --- a/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/utils/TruthExt.kt @@ -9,7 +9,24 @@ fun assertEither(actual: Either, expected: Either) { + actual + .onRight { Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) } + .onLeft { + error("Actual is Either.Left: $it") + } +} + +fun assertEitherLeft(actual: Either, expected: Throwable) { + actual + .onRight { error("Actual is Either.Right: $it") } + .onLeft { + Truth.assertThat(it::class.java).isEqualTo(expected::class.java) + Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index abf8a0a3ef..2e21a957da 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -1,15 +1,11 @@ package com.tangem.data.staking.multi import arrow.core.Either -import arrow.core.getOrElse import arrow.core.left -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.ensure +import arrow.core.right import arrow.core.toOption import com.tangem.data.common.api.safeApiCall import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody @@ -36,7 +32,6 @@ import javax.inject.Inject * @property userWalletsStore user wallets store * @property stakingYieldsStore staking yields store * @property yieldsBalancesStore yields balances store - * @property stakingIdFactory factory for creating StakingID * @property stakeKitApi stake kit API * @property dispatchers dispatchers * @@ -46,7 +41,6 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, - private val stakingIdFactory: StakingIdFactory, private val stakeKitApi: StakeKitApi, private val dispatchers: CoroutineDispatcherProvider, ) : MultiYieldBalanceFetcher { @@ -54,11 +48,12 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either { Timber.i("Start fetching yield balances for params:\n$params") - checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { - return it.left() + val stakingIds = params.stakingIds.ifEmpty { + Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") + return Unit.right() } - val stakingIds = getStakingIds(params).getOrElse { + checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { return it.left() } @@ -94,30 +89,6 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( } } - private suspend fun getStakingIds(params: MultiYieldBalanceFetcher.Params) = either { - val stakingIds = catch( - block = { - params.currencyIdWithNetworkMap.mapNotNullTo(hashSetOf()) { (currencyId, network) -> - stakingIdFactory.create( - userWalletId = params.userWalletId, - currencyId = currencyId, - network = network, - ) - } - }, - catch = ::raise, - ) - - ensure(stakingIds.isNotEmpty()) { - val exception = IllegalStateException("Unable to create staking ids for $params: list is empty") - Timber.e(exception) - - raise(exception) - } - - stakingIds - } - private suspend fun getAvailableStakingIds(userWalletId: UserWalletId, stakingIds: Set): Set { val yieldIds = getYieldsIds(userWalletId = userWalletId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt index 4f507e4739..1666b20e35 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt @@ -20,9 +20,7 @@ internal class DefaultSingleYieldBalanceFetcher @Inject constructor( return multiYieldBalanceFetcher( params = MultiYieldBalanceFetcher.Params( userWalletId = params.userWalletId, - currencyIdWithNetworkMap = mapOf( - params.currencyId to params.network, - ), + stakingIds = setOf(params.stakingId), ), ) } diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt index d83b1481ee..0f02079987 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -1,14 +1,12 @@ package com.tangem.data.staking.multi import arrow.core.toOption -import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.test.utils.assertEitherLeft +import com.tangem.common.test.utils.assertEitherRight import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError @@ -34,55 +32,46 @@ internal class DefaultMultiYieldBalanceFetcherTest { private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() - private val yieldsBalancesStore: YieldsBalancesStore = mockk() - private val stakingIdFactory: StakingIdFactory = mockk() + private val yieldsBalancesStore: YieldsBalancesStore = mockk(relaxUnitFun = true) private val stakeKitApi: StakeKitApi = mockk() private val fetcher = DefaultMultiYieldBalanceFetcher( userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, - stakingIdFactory = stakingIdFactory, stakeKitApi = stakeKitApi, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakingIdFactory, stakeKitApi) + clearMocks(userWalletsStore, stakingYieldsStore, yieldsBalancesStore, stakeKitApi) } @Test fun `fetch yields balances successfully`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId } + val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create) val result = setOf( MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId), MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId), ) + coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result) - coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -91,40 +80,30 @@ internal class DefaultMultiYieldBalanceFetcherTest { coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } - Truth.assertThat(actual.isRight()).isTrue() + assertEitherRight(actual) } @Test fun `fetch yields balances successfully if one of stakingIds is unavailable`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) } just Runs - val requests = listOf(YieldBalanceRequestBodyFactory.create(tonId)) val result = setOf(MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId)) coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result) - coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) @@ -132,15 +111,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } - Truth.assertThat(actual.isRight()).isTrue() + assertEitherRight(actual) } @Test fun `fetch yields balances failure if user wallet is not supported`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet @@ -149,10 +126,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher.invoke(params) // Assert - coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -162,17 +138,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null @@ -180,10 +152,9 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher.invoke(params) // Assert - coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) @@ -193,69 +164,23 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) - } - - @Test - fun `fetch yields balances failure if stakingIdFactory returns null`() = runTest { - // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) - - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns null - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns null - - // Actual - val actual = fetcher.invoke(params) - - // Assert - coVerify { - userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) - } - - coVerify(inverse = true) { - yieldsBalancesStore.refresh(any(), any>()) - stakingYieldsStore.getSyncWithTimeout() - stakeKitApi.getMultipleYieldBalances(any()) - yieldsBalancesStore.storeActual(any(), any()) - yieldsBalancesStore.storeError(any(), any()) - } - - val expected = IllegalStateException("Unable to create staking ids for $params: list is empty") - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -268,33 +193,23 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -307,38 +222,28 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if yields converting is failed`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf( MockYieldDTOFactory.create(tonId).copy(id = null), MockYieldDTOFactory.create(solanaId).copy(id = null), ) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -351,35 +256,25 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -394,47 +289,37 @@ internal class DefaultMultiYieldBalanceFetcherTest { """ No available yields to fetch yield balances: – userWalletId: $userWalletId - – stakingIds: ${setOf(solanaId, tonId).joinToString()} + – stakingIds: ${tonAndSolanaIds.joinToString()} """.trimIndent(), ) - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } @Test fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { // Arrange - val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) - - val params = MultiYieldBalanceFetcher.Params(userWalletId, currencyIdWithNetworkMap) + val params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet - coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns tonId - coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns solanaId - coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields - val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create) + val requests = setOf(tonId, solanaId).map(YieldBalanceRequestBodyFactory::create) @Suppress("UNCHECKED_CAST") val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse> coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse - coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs // Actual val actual = fetcher.invoke(params) // Assert - coVerify { + coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) - stakingIdFactory.create(params.userWalletId, ton.id, ton.network) - stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -445,20 +330,13 @@ internal class DefaultMultiYieldBalanceFetcherTest { val expected = ApiResponseError.NetworkException - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + assertEitherLeft(actual, expected) } private companion object { val userWalletId = UserWalletId("011") val userWallet = MockUserWalletFactory.create() - val mocks = MockCryptoCurrencyFactory() - - val ton = mocks.createCoin(Blockchain.TON) - val solana = mocks.createCoin(Blockchain.Solana) - val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt index 7bf536b73d..6722fc8d5f 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt @@ -3,8 +3,7 @@ package com.tangem.data.staking.single import arrow.core.left import arrow.core.right import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -37,15 +36,11 @@ internal class DefaultSingleYieldBalanceFetcherTest { @Test fun `fetch yield balance successfully`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = ton.id, - network = ton.network, - ) + val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) val multiParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = mapOf(ton.id to ton.network), + stakingIds = setOf(tonId), ) val multiResult = Unit.right() @@ -64,16 +59,9 @@ internal class DefaultSingleYieldBalanceFetcherTest { @Test fun `fetch yield balance failure`() = runTest { // Arrange - val params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = ton.id, - network = ton.network, - ) + val params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = tonId) - val multiParams = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = mapOf(ton.id to ton.network), - ) + val multiParams = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = setOf(tonId)) val multiResult = IllegalStateException().left() @@ -89,6 +77,6 @@ internal class DefaultSingleYieldBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") - val ton = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId } } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 1c78e3949d..065f27f412 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -23,6 +24,7 @@ class SaveManagedTokensUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( @@ -91,11 +93,12 @@ class SaveManagedTokensUseCase( userWalletId: UserWalletId, addedCurrencies: List, ) { + val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = addedCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index d5bbb3f689..47e769d11f 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,6 +29,7 @@ class SaveMarketTokensUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( @@ -89,11 +91,12 @@ class SaveMarketTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { + val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 24b94b0ff2..0308e969e5 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -1,36 +1,41 @@ package com.tangem.domain.staking import arrow.core.Either -import arrow.core.raise.catch +import arrow.core.getOrElse import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher class FetchStakingYieldBalanceUseCase( - private val stakingErrorResolver: StakingErrorResolver, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return either { - catch( - block = { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), - ) - }, - catch = { stakingErrorResolver.resolve(it) }, - ) - } + ): Either = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .mapLeft { StakingError.DomainError("$it") } + .bind() } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt index 397dd9f7de..17330db5b0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingID /** * Fetcher of yields balances @@ -15,23 +14,19 @@ interface MultiYieldBalanceFetcher : FlowFetcher, + val stakingIds: Set, ) { override fun toString(): String { - val currencyIdWithNetworkMap = currencyIdWithNetworkMap.entries.joinToString { - "${it.key.value} - ${it.value}" - } - return """ MultiYieldBalanceFetcher.Params( userWalletId = $userWalletId, - currencyIdWithNetworkMap: $currencyIdWithNetworkMap + stakingIds: ${stakingIds.joinToString()} ) """.trimIndent() } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt index ffff57fcff..789c93584a 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowFetcher -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.StakingID /** * Fetcher of yield balance @@ -16,12 +15,10 @@ interface SingleYieldBalanceFetcher : FlowFetcher = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = addedCurrency.id, + network = addedCurrency.network, ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .bind() } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 82b0862695..2c491ffdee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -6,12 +6,13 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -21,6 +22,7 @@ class FetchCardTokenListUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -83,11 +85,12 @@ class FetchCardTokenListUseCase( } private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 6cb6d54c1d..e500c445fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -1,18 +1,20 @@ package com.tangem.domain.tokens import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.right import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -32,6 +34,7 @@ class FetchCurrencyStatusUseCase( private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -136,14 +139,25 @@ class FetchCurrencyStatusUseCase( private suspend fun fetchStakingBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), + ): Either = either { + val stakingId = stakingIdFactory.create( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, ) + .getOrElse { + when (it) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$it")) + StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() + } + + return@either + } + + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), + ) + .bind() } private fun List>.summarizeResult(): Either { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 6206a7203b..d4df6acfcd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -8,12 +8,13 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -29,6 +30,7 @@ class FetchTokenListUseCase( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, ) { /** @@ -104,11 +106,12 @@ class FetchTokenListUseCase( } private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 5c5c5d91b2..6302bef5ef 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -216,10 +216,14 @@ class CachedCurrenciesStatusesOperations( ) }, async { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( params = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = stakingIds, ), ) }, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 4e6677d2c0..bceb16c296 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -1,11 +1,14 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either +import arrow.core.raise.either import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier @@ -13,7 +16,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -43,6 +45,7 @@ class WalletBalanceFetcher internal constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -54,6 +57,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( currenciesRepository = currenciesRepository, @@ -68,6 +72,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -145,13 +150,27 @@ class WalletBalanceFetcher internal constructor( private suspend fun fetchStaking( userWalletId: UserWalletId, currencies: Set, - ): Either { - return multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) + ): Either = either { + val maybeStakingIds = currencies.map { + val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it) + + if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { + Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${it.id}") + } + + stakingId + } + + val stakingIds = maybeStakingIds.mapNotNullTo(hashSetOf()) { it.getOrNull() } + + if (stakingIds.isNotEmpty()) { + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), + ) + .bind() + } else { + Timber.i("No staking IDs found for user wallet $userWalletId with currencies: $currencies") + } } /** diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index d5d5f14816..f7cc8a5633 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -1,20 +1,25 @@ package com.tangem.domain.tokens.wallet +import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.assertEither +import com.tangem.common.test.utils.assertEitherRight import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.wallet.FetchingSource.* import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest @@ -37,6 +42,7 @@ internal class WalletBalanceFetcherTest { private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( currenciesRepository = currenciesRepository, @@ -46,6 +52,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, + stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -83,6 +90,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -113,6 +121,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -146,6 +155,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -177,6 +187,7 @@ internal class WalletBalanceFetcherTest { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -222,6 +233,7 @@ internal class WalletBalanceFetcherTest { singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiQuoteStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -267,6 +279,7 @@ internal class WalletBalanceFetcherTest { singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) multiNetworkStatusFetcher(params = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -281,7 +294,7 @@ internal class WalletBalanceFetcherTest { val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) val exception = IllegalStateException("Error") @@ -289,6 +302,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() // Act @@ -305,6 +324,8 @@ internal class WalletBalanceFetcherTest { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -316,6 +337,131 @@ internal class WalletBalanceFetcherTest { } } + @Test + fun `fetch failure if stakingIdFactory RETURNS UnsupportedCurrency for all currencies`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) + } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if stakingIdFactory RETURNS UnableToGetAddress for all currencies`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val stakingId = Either.Left( + StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + ) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if stakingIdFactory RETURNS UnableToGetAddress and UnsupportedCurrency`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val ethereumStakingId = Either.Left( + StakingIdFactory.Error.UnableToGetAddress(integrationId = StakingIntegrationID.EthereumToken.Polygon), + ) + val stellarStakingId = Either.Left(StakingIdFactory.Error.UnsupportedCurrency) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns ethereumStakingId + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns stellarStakingId + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + assertEitherRight(actual) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + @Test fun `fetch failure if all fetching sources RETURNS LEFT`() = runTest { // Arrange @@ -337,7 +483,7 @@ internal class WalletBalanceFetcherTest { val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) val exception = IllegalStateException("Error") @@ -347,6 +493,12 @@ internal class WalletBalanceFetcherTest { every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() // Act @@ -367,6 +519,8 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources multiNetworkStatusFetcher(params = networkStatusFetcherParams) multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -397,7 +551,7 @@ internal class WalletBalanceFetcherTest { val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + stakingIds = setOf(ethereumStakingId, stellarStakingId), ) every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver @@ -405,6 +559,12 @@ internal class WalletBalanceFetcherTest { every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + } returns Either.Right(ethereumStakingId) + coEvery { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) + } returns Either.Right(stellarStakingId) coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns Unit.right() // Act @@ -420,6 +580,8 @@ internal class WalletBalanceFetcherTest { multiWalletBalanceFetcher.fetchingSources multiNetworkStatusFetcher(params = networkStatusFetcherParams) multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.ethereum) + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyFactory.stellar) multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } @@ -475,6 +637,7 @@ internal class WalletBalanceFetcherTest { coVerify(inverse = true) { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -524,6 +687,7 @@ internal class WalletBalanceFetcherTest { coVerify(inverse = true) { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + stakingIdFactory.create(userWalletId = any(), cryptoCurrency = any()) multiYieldBalanceFetcher(params = any()) } } @@ -531,5 +695,7 @@ internal class WalletBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") + val ethereumStakingId = StakingID(integrationId = "ethereum", address = "0x1") + val stellarStakingId = StakingID(integrationId = "stellar", address = "0x1") } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index e88a4566e0..ab80da33c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -15,19 +15,20 @@ import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase +import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.models.UnlockWalletsError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase @@ -113,6 +114,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { @@ -399,11 +401,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( .onLeft { Timber.e("Unable to fetch quotes: $it") } }, async { + val stakingIds = currencies.mapNotNullTo(hashSetOf()) { + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() + } + multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associate { it.id to it.network }, - ), + params = MultiYieldBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), ) .onLeft { Timber.e("Unable to fetch yield balances: $it") } }, From 03f4f51fc7e9fc1439c704af0c94fc2cce3e4843 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Jul 2025 09:54:30 +0400 Subject: [PATCH 007/165] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 9 +- .../data/staking/DefaultStakingRepository.kt | 37 ---- .../data/staking/di/StakingDataModule.kt | 3 - .../data/staking/utils/StakingIdFactory.kt | 77 ------- .../staking/utils/StakingIdFactoryTest.kt | 188 ------------------ .../staking/repositories/StakingRepository.kt | 15 -- .../BaseCurrencyStatusOperations.kt | 61 +++--- .../CachedCurrenciesStatusesOperations.kt | 6 +- 8 files changed, 42 insertions(+), 354 deletions(-) delete mode 100644 data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt delete mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 70d26f85a2..5ee93bbafa 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceSupplier @@ -404,7 +405,6 @@ internal object TokensDomainModule { tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -412,6 +412,7 @@ internal object TokensDomainModule { multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceSupplier: MultiYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, @@ -419,7 +420,6 @@ internal object TokensDomainModule { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, - stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -427,6 +427,7 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, @@ -440,7 +441,6 @@ internal object TokensDomainModule { tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -448,6 +448,7 @@ internal object TokensDomainModule { multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceSupplier: MultiYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, @@ -455,7 +456,6 @@ internal object TokensDomainModule { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, - stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -463,6 +463,7 @@ internal object TokensDomainModule { multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles = tokensFeatureToggles, diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 15787789bf..dfb797e129 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -20,7 +20,6 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionConverte import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.data.staking.store.YieldsBalancesStore -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi @@ -60,7 +59,6 @@ import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import timber.log.Timber -import java.math.BigDecimal @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( @@ -71,7 +69,6 @@ internal class DefaultStakingRepository( private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val stakingIdFactory: StakingIdFactory, moshi: Moshi, ) : StakingRepository { @@ -375,32 +372,6 @@ internal class DefaultStakingRepository( } } - override suspend fun getSingleYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance { - val stakingId = stakingIdFactory.create( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ) ?: error("Could not create stakingId") - - return stakingBalanceStoreV2.getSyncOrNull(userWalletId = userWalletId, stakingId = stakingId) - ?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address) - } - - override suspend fun getMultiYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): List? { - val stakingIds = cryptoCurrencies.mapNotNull { - stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) - } - - return stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) - ?.filter { it.getStakingId() in stakingIds } - } - override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.default) { val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false @@ -415,14 +386,6 @@ internal class DefaultStakingRepository( } } - override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? { - return when { - stakingIdFactory.isPolygonIntegrationId(integrationId) && - stakingActionType == StakingActionType.CLAIM_REWARDS -> BigDecimal.ONE - else -> null - } - } - private suspend fun createActionRequestBody( userWalletId: UserWalletId, network: Network, diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 45d3cac792..a5c5a41e2b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -10,7 +10,6 @@ import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles -import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi @@ -45,7 +44,6 @@ internal object StakingDataModule { walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, - stakingIdFactory: StakingIdFactory, @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( @@ -57,7 +55,6 @@ internal object StakingDataModule { getUserWalletUseCase = getUserWalletUseCase, stakingFeatureToggles = stakingFeatureToggles, moshi = moshi, - stakingIdFactory = stakingIdFactory, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt deleted file mode 100644 index 02c5f0b9a2..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.data.staking.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.blockchainsdk.utils.toMigratedCoinId -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.walletmanager.WalletManagersFacade -import javax.inject.Inject - -/** - * Factory of [StakingID] - * - * @property walletManagersFacade wallet manager facade - * -[REDACTED_AUTHOR] - */ -internal class StakingIdFactory @Inject constructor( - private val walletManagersFacade: WalletManagersFacade, -) { - - suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): StakingID? { - val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - val integrationId = createIntegrationId(currencyId) - - if (address == null || integrationId == null) return null - - return StakingID(integrationId = integrationId, address = address) - } - - fun createIntegrationId(currencyId: CryptoCurrency.ID): String? { - val integrationKey = with(currencyId) { rawNetworkId.plus(rawCurrencyId) } - return integrationIdMap[integrationKey] - } - - fun isPolygonIntegrationId(integrationId: String): Boolean = integrationId == ETHEREUM_POLYGON_INTEGRATION_ID - - @Suppress("UnusedPrivateMember", "unused") - companion object { - - private const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" - private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - private const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" - private const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" - private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - private const val NEAR_INTEGRATION_ID = "near-near-native-staking" - private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" - private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking" - - // uncomment items as implementation is ready - private val integrationIdMap = mapOf( - Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID, - Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID, - Blockchain.Tron.toDefaultKey() to TRON_INTEGRATION_ID, - Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - Blockchain.BSC.toDefaultKey() to BINANCE_INTEGRATION_ID, - // Blockchain.Polkadot.toDefaultKey() to POLKADOT_INTEGRATION_ID, - // Blockchain.Avalanche.toDefaultKey() to AVALANCHE_INTEGRATION_ID, - // Blockchain.Cronos.toDefaultKey() to CRONOS_INTEGRATION_ID, - // Blockchain.Kava.toDefaultKey() to KAVA_INTEGRATION_ID, - // Blockchain.Near.toDefaultKey() to NEAR_INTEGRATION_ID, - // Blockchain.Tezos.toDefaultKey() to TEZOS_INTEGRATION_ID, - Blockchain.Cardano.toDefaultKey() to CARDANO_INTEGRATION_ID, - ) - - private fun Blockchain.toDefaultKey(): String = id + toCoinId() - } -} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt deleted file mode 100644 index 9244ac2684..0000000000 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/utils/StakingIdFactoryTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.tangem.data.staking.utils - -import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.common.test.utils.ProvideTestModels -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.walletmanager.WalletManagersFacade -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.params.ParameterizedTest - -/** -[REDACTED_AUTHOR] - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class StakingIdFactoryTest { - - private val walletManagersFacade: WalletManagersFacade = mockk() - private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade) - - @BeforeEach - fun resetMocks() { - clearMocks(walletManagersFacade) - } - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class CreateIntegrationId { - - @ParameterizedTest - @ProvideTestModels - fun createIntegrationId(model: CreateIntegrationIdModel) { - // Act - val actual = factory.createIntegrationId(currencyId = model.currencyId) - - // Assert - Truth.assertThat(actual).isEqualTo(model.expected) - } - - private fun provideTestModels() = listOf( - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = "ton-ton-chorus-one-pools-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = "solana-sol-native-multivalidator-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = "cosmos-atom-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = "tron-trx-native-staking", - ), - CreateIntegrationIdModel( - currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"), - expected = "ethereum-matic-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = "bsc-bnb-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = "cardano-ada-native-staking", - ), - CreateIntegrationIdModel( - currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), - expected = null, - ), - ) - } - - data class CreateIntegrationIdModel(val currencyId: CryptoCurrency.ID, val expected: String?) - - @Nested - @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class Create { - - private val defaultAddress = "address" - - @Test - fun `create returns null if address is null`() = runTest { - // Arrange - val userWalletId = UserWalletId(stringValue = "011") - val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) - - coEvery { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) - } returns null - - // Act - val actual = factory.create( - userWalletId = userWalletId, - currencyId = currency.id, - network = currency.network, - ) - - // Assert - val expected = null - Truth.assertThat(actual).isEqualTo(expected) - - coVerify(exactly = 1) { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = currency.network) - } - } - - @ParameterizedTest - @ProvideTestModels - fun create(model: CreateModel) = runTest { - // Arrange - val userWalletId = UserWalletId(stringValue = "011") - val network = MockCryptoCurrencyFactory().createCoin(Blockchain.TON).network - - coEvery { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - } returns defaultAddress - - // Act - val actual = factory.create(userWalletId = userWalletId, currencyId = model.currencyId, network = network) - - // Assert - Truth.assertThat(actual).isEqualTo(model.expected) - - coVerify(exactly = 1) { - walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) - } - } - - private fun provideTestModels() = listOf( - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.TON), - expected = createStakingId(integrationId = "ton-ton-chorus-one-pools-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Solana), - expected = createStakingId(integrationId = "solana-sol-native-multivalidator-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cosmos), - expected = createStakingId(integrationId = "cosmos-atom-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Tron), - expected = createStakingId(integrationId = "tron-trx-native-staking"), - ), - CreateModel( - currencyId = CryptoCurrency.ID.fromValue(value = "coin⟨ETH⟩polygon-ecosystem-token⚓"), - expected = createStakingId(integrationId = "ethereum-matic-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.BSC), - expected = createStakingId(integrationId = "bsc-bnb-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Cardano), - expected = createStakingId(integrationId = "cardano-ada-native-staking"), - ), - CreateModel( - currencyId = createCurrencyId(blockchain = Blockchain.Bitcoin), - expected = null, - ), - ) - - private fun createStakingId(integrationId: String): StakingID { - return StakingID(integrationId = integrationId, address = defaultAddress) - } - } - - data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingID?) - - private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID { - return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩${blockchain.toCoinId()}⚓") - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index dcb09a9010..3be0818547 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -10,15 +10,12 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import kotlinx.coroutines.flow.Flow -import java.math.BigDecimal @Suppress("TooManyFunctions") interface StakingRepository { @@ -45,13 +42,6 @@ interface StakingRepository { stakingActionStatus: StakingActionStatus, ): List - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance - - suspend fun getMultiYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): List? - suspend fun createAction(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingAction suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate @@ -64,9 +54,4 @@ interface StakingRepository { ): Pair suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean - - /** - * Return action requirement amount - */ - fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index d3c6c507a6..9aaf902fa7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -1,10 +1,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.recover +import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency @@ -23,7 +20,8 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.isStakingSupported import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer @@ -39,7 +37,6 @@ import kotlinx.coroutines.flow.* * Base operations for working with currency status * * @property currenciesRepository repository for currencies - * @property stakingRepository repository for staking * [REDACTED_AUTHOR] */ @@ -47,11 +44,11 @@ import kotlinx.coroutines.flow.* abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, @@ -411,34 +408,44 @@ abstract class BaseCurrencyStatusOperations( private suspend fun getYieldBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Either> { - return catch( - block = { - val balances = stakingRepository.getMultiYieldBalanceSync( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ) + ): Either> = either { + val stakingIds = cryptoCurrencies.mapNotNull { cryptoCurrency -> + stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) + .getOrNull() + } - if (balances.isNullOrEmpty()) { - Error.EmptyYieldBalances.left() - } else { - balances.right() - } - }, - catch = { Error.EmptyYieldBalances.left() }, + ensure(stakingIds.isNotEmpty()) { Error.EmptyYieldBalances } + + val balances = multiYieldBalanceSupplier.getSyncOrNull( + params = MultiYieldBalanceProducer.Params(userWalletId = userWalletId), ) + .orEmpty() + .filter { it.getStakingId() in stakingIds } + + ensure(balances.isNotEmpty()) { Error.EmptyYieldBalances } + + balances } private suspend fun getYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Either { - return catch( - block = { stakingRepository.getSingleYieldBalanceSync(userWalletId, cryptoCurrency).right() }, - catch = { - Error.EmptyYieldBalances.left() - }, + ): Either = either { + val stakingId = stakingIdFactory.create(userWalletId, cryptoCurrency) + .mapLeft { + val exception = IllegalStateException("$it") + Error.DataError(exception) + } + .bind() + + val yieldBalance = singleYieldBalanceSupplier.getSyncOrNull( + params = SingleYieldBalanceProducer.Params( + userWalletId = userWalletId, + stakingId = stakingId, + ), ) + + ensureNotNull(yieldBalance) { Error.EmptyYieldBalances } } private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 6302bef5ef..0948480baa 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -28,7 +28,7 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher -import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier @@ -47,7 +47,6 @@ import kotlinx.coroutines.flow.* class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -55,6 +54,7 @@ class CachedCurrenciesStatusesOperations( private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, @@ -63,11 +63,11 @@ class CachedCurrenciesStatusesOperations( BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, - stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, stakingIdFactory = stakingIdFactory, tokensFeatureToggles = tokensFeatureToggles, From 2f91afa9223571dcc836f2547b6684ea14dc8785 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 10:57:51 +0300 Subject: [PATCH 008/165] Updated on 2026-08-14 --- .../com/tangem/tap/di/data/CardDataModule.kt | 34 ----- .../tangem/tap/di/domain/CardDomainModule.kt | 5 +- .../tap/di/domain/ManageTokensDomainModule.kt | 2 +- .../tap/di/domain/MarketsDomainModule.kt | 2 +- .../tap/di/domain/TransactionDomainModule.kt | 2 +- .../tangem/tap/di/hot/TangemHotSdkModule.kt | 6 - .../domain/hot/HotWalletPasswordRequester.kt | 9 -- .../tangem/tap/domain/hot/TangemHotSigner.kt | 78 ---------- .../sdk/impl/DefaultTangemSdkManager.kt | 2 +- .../tasks/product/CreateProductWalletTask.kt | 2 +- .../domain/tasks/product/DerivationsFinder.kt | 2 +- .../domain/tasks/product/ScanProductTask.kt | 4 +- .../visa/VisaCustomerWalletApproveTask.kt | 2 +- .../hot/DefaultHotWalletPasswordRequester.kt | 13 -- .../domain/token/MockCryptoCurrencyFactory.kt | 4 +- .../data/common/network/NetworkFactory.kt | 4 +- .../data/common/network/NetworkFactoryTest.kt | 4 +- data/manage-tokens/build.gradle.kts | 1 + .../DefaultCustomTokensRepository.kt | 43 ++++-- .../utils/ManagedCryptoCurrencyFactory.kt | 4 +- data/visa/build.gradle.kts | 1 + .../data/visa/utils/VisaCurrencyFactory.kt | 5 +- .../walletmanager/WalletManagerFactory.kt | 4 +- data/wallets/build.gradle.kts | 12 +- .../DefaultColdMapDerivationsRepository.kt | 130 +++++++--------- .../cold/UserWalletIdPreflightReadFilter.kt | 25 ++++ .../DefaultDerivationsRepository.kt | 95 ++++++++++++ .../derivations}/MissedDerivationsFinder.kt | 58 +++++--- .../data/wallets/di/WalletsDataModule.kt | 24 +++ .../hot/DefaultHotMapDerivationsRepository.kt | 139 ++++++++++++++++++ .../data/wallets}/hot/HotWalletAccessor.kt | 24 +-- .../wallets}/hot/TangemHotWalletSigner.kt | 2 +- .../DefaultDerivationsRepositoryTest.kt | 47 +++--- .../wallets/derivations}/DerivedKeysMocks.kt | 2 +- .../MissedDerivationsFinderTest.kt | 19 +-- .../domain/card/TangemCardTypesResolver.kt | 3 +- .../card/common/util/ScanResponseExt.kt | 15 -- domain/manage-tokens/build.gradle.kts | 1 + .../managetokens/SaveManagedTokensUseCase.kt | 2 +- .../domain/markets/SaveMarketTokensUseCase.kt | 2 +- .../wallets/builder/HotUserWalletBuilder.kt | 2 +- .../ColdMapDerivationsRepository.kt | 35 +++++ .../derivations}/DerivationStyleProvider.kt | 3 +- .../derivations/DerivationStyleProviderExt.kt | 17 +++ .../derivations}/DerivationsRepository.kt | 4 +- .../HotMapDerivationsRepository.kt | 35 +++++ .../hot}/HotWalletPasswordRequester.kt | 2 +- .../repository/HotDerivationsRepository.kt | 8 - .../usecase}/DerivePublicKeysUseCase.kt | 7 +- .../GetExtendedPublicKeyForCurrencyUseCase.kt | 4 +- .../usecase}/HasMissedDerivationsUseCase.kt | 12 +- features/hot-wallet/api/build.gradle.kts | 1 + .../HotAccessCodeRequestComponent.kt | 1 + .../DefaultHotAccessCodeRequestComponent.kt | 3 +- .../HotAccessCodeRequestModel.kt | 2 +- .../di/ComponentModuleBinds.kt | 2 +- .../proxy/HotWalletPasswordRequesterProxy.kt | 2 +- features/manage-tokens/impl/build.gradle.kts | 1 + .../model/CustomTokenFormModel.kt | 4 +- .../managetokens/model/ManageTokensModel.kt | 2 +- .../model/OnboardingManageTokensModel.kt | 2 +- .../impl/model/MarketsPortfolioModel.kt | 2 +- .../model/OnrampAddToPortfolioModel.kt | 2 +- .../referral/domain/ReferralInteractorImpl.kt | 2 +- .../domain/di/ReferralDomainModule.kt | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 2 +- .../intents/WalletWarningsClickIntents.kt | 2 +- 67 files changed, 623 insertions(+), 371 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt rename app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt => data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt (56%) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt rename {app/src/main/java/com/tangem/tap/domain/card => data/wallets/src/main/java/com/tangem/data/wallets/derivations}/MissedDerivationsFinder.kt (64%) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt rename {app/src/main/java/com/tangem/tap/domain => data/wallets/src/main/java/com/tangem/data/wallets}/hot/HotWalletAccessor.kt (84%) rename {app/src/main/java/com/tangem/tap/domain => data/wallets/src/main/java/com/tangem/data/wallets}/hot/TangemHotWalletSigner.kt (99%) rename {app/src/test/kotlin/com/tangem/tap/domain/card => data/wallets/src/test/java/com/tangem/data/wallets/derivations}/DefaultDerivationsRepositoryTest.kt (80%) rename {app/src/test/kotlin/com/tangem/tap/domain/card => data/wallets/src/test/java/com/tangem/data/wallets/derivations}/DerivedKeysMocks.kt (95%) rename {app/src/test/kotlin/com/tangem/tap/domain/card => data/wallets/src/test/java/com/tangem/data/wallets/derivations}/MissedDerivationsFinderTest.kt (90%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt rename domain/{card/src/main/kotlin/com/tangem/domain/card => wallets/src/main/java/com/tangem/domain/wallets/derivations}/DerivationStyleProvider.kt (92%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt rename domain/{card/src/main/kotlin/com/tangem/domain/card/repository => wallets/src/main/java/com/tangem/domain/wallets/derivations}/DerivationsRepository.kt (92%) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt rename {features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet => domain/wallets/src/main/java/com/tangem/domain/wallets/hot}/HotWalletPasswordRequester.kt (91%) delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt rename domain/{card/src/main/kotlin/com/tangem/domain/card => wallets/src/main/java/com/tangem/domain/wallets/usecase}/DerivePublicKeysUseCase.kt (74%) rename domain/{card/src/main/kotlin/com/tangem/domain/card => wallets/src/main/java/com/tangem/domain/wallets/usecase}/GetExtendedPublicKeyForCurrencyUseCase.kt (98%) rename domain/{card/src/main/kotlin/com/tangem/domain/card => wallets/src/main/java/com/tangem/domain/wallets/usecase}/HasMissedDerivationsUseCase.kt (75%) diff --git a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt deleted file mode 100644 index 788367340b..0000000000 --- a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.tap.di.data - -import com.tangem.data.common.network.NetworkFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.sdk.api.TangemSdkManager -import com.tangem.tap.domain.card.DefaultDerivationsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CardDataModule { - - @Singleton - @Provides - fun providesDerivationsRepository( - tangemSdkManager: TangemSdkManager, - userWalletsStore: UserWalletsStore, - networkFactory: NetworkFactory, - dispatchers: CoroutineDispatcherProvider, - ): DerivationsRepository { - return DefaultDerivationsRepository( - tangemSdkManager = tangemSdkManager, - userWalletsStore = userWalletsStore, - networkFactory = networkFactory, - dispatchers = dispatchers, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index fc0942279a..0aa400353e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -2,11 +2,14 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.* import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.sdk.api.TangemSdkManager diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 0db474821f..89bd3b4ca9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -1,6 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 71f6985831..1bb8a1ec1c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index eb56a8d16c..3538d33584 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.data.wallets.hot.TangemHotWalletSigner import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher @@ -11,7 +12,6 @@ import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.tap.domain.hot.TangemHotWalletSigner import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt index f68e642f94..c21b55928b 100644 --- a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt +++ b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt @@ -1,8 +1,6 @@ package com.tangem.tap.di.hot import com.tangem.hot.sdk.TangemHotSdk -import com.tangem.tap.domain.hot.HotWalletPasswordRequester -import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester import com.tangem.tap.features.hot.TangemHotSDKProxy import dagger.Binds import dagger.Module @@ -17,8 +15,4 @@ internal interface TangemHotSdkModule { @Binds @Singleton fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk - - @Binds - @Singleton - fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt deleted file mode 100644 index 1f80fd785f..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.domain.hot - -import com.tangem.hot.sdk.model.HotAuth -import com.tangem.hot.sdk.model.HotWalletId - -interface HotWalletPasswordRequester { - - suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt deleted file mode 100644 index 0b71ca974c..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.tangem.tap.domain.hot - -import com.tangem.blockchain.common.TransactionSigner -import com.tangem.blockchain.common.Wallet -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError -import com.tangem.common.map -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.hot.sdk.model.DataToSign -import com.tangem.operations.sign.SignData -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -class TangemHotSigner @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Hot, - private val hotWalletAccessor: HotWalletAccessor, -) : TransactionSigner { - - override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult { - return sign(listOf(hash), publicKey).map { it.first() } - } - - override suspend fun sign( - hashes: List, - publicKey: Wallet.PublicKey, - ): CompletionResult> { - val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey } - ?: return CompletionResult.Failure( - TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), - ) - - val result = hotWalletAccessor.signHashes( - hotWalletId = userWallet.hotWalletId, - dataToSign = listOf( - DataToSign( - curve = wallet.curve, - hashes = hashes, - derivationPath = publicKey.derivationPath, - ), - ), - ) - - return CompletionResult.Success(result.map { it.signatures }.flatten()) - } - - override suspend fun multiSign( - dataToSign: List, - publicKey: Wallet.PublicKey, - ): CompletionResult> { - val result = hotWalletAccessor.signHashes( - hotWalletId = userWallet.hotWalletId, - dataToSign = dataToSign.map { signData -> - val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey } - ?: return CompletionResult.Failure( - TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), - ) - - DataToSign( - curve = wallet.curve, - hashes = listOf(signData.hash), - derivationPath = signData.derivationPath, - ) - }, - ) - - return CompletionResult.Success( - result.mapIndexed { index, data -> - dataToSign[index].publicKey to data.signatures.first() - }.toMap(), - ) - } - - @AssistedFactory - interface Factory { - fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 83c7a9c1fb..aaf4f87888 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -20,7 +20,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index acd5a2e14a..fba95394c0 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -14,7 +14,7 @@ import com.tangem.common.map import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index 6e6a912b0d..fa255fdacb 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWalletId diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 90275890f2..df05ce2f7e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.common.TwinsHelper -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 3ef65f5c16..a44a04e051 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -16,7 +16,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation diff --git a/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt deleted file mode 100644 index b9d1c6ae6c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.features.hot - -import com.tangem.hot.sdk.model.HotAuth -import com.tangem.hot.sdk.model.HotWalletId -import com.tangem.tap.domain.hot.HotWalletPasswordRequester -import javax.inject.Inject - -class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester { - - override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password { - return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY] - } -} \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index efba92dc00..ee2579d3ca 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -8,8 +8,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.card.DerivationStyleProvider -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index bd48e9cea2..e518072440 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -6,11 +6,11 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import timber.log.Timber import javax.inject.Inject diff --git a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt index 766643ffe0..4cbcb9a0a5 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt @@ -7,10 +7,10 @@ import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.common.test.utils.ProvideTestModels -import com.tangem.domain.card.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index c0da8f1be9..9095c4f1ab 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.manageTokens) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.legacy) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index d311b44167..34beb0198c 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -22,8 +22,8 @@ import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -247,26 +247,43 @@ internal class DefaultCustomTokensRepository( val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "User wallet [$userWalletId] not found while getting supported networks" } - val scanResponse = userWallet.requireColdWallet().scanResponse // TODO [REDACTED_TASK_KEY] - Blockchain.entries - .mapNotNull { blockchain -> - val canHandleBlockchain = scanResponse.card.canHandleBlockchain( - blockchain, - scanResponse.cardTypesResolver, - excludedBlockchains, - ) + when (userWallet) { + is UserWallet.Hot -> { + Blockchain.entries.mapNotNull { + // TODO: refactor [REDACTED_JIRA]\ + if (it.isTestnet() || it in excludedBlockchains) return@mapNotNull null - if (canHandleBlockchain) { networkFactory.create( - blockchain = blockchain, + blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) - } else { - null } } + is UserWallet.Cold -> { + val scanResponse = userWallet.scanResponse + + Blockchain.entries + .mapNotNull { blockchain -> + val canHandleBlockchain = scanResponse.card.canHandleBlockchain( + blockchain, + scanResponse.cardTypesResolver, + excludedBlockchains, + ) + + if (canHandleBlockchain) { + networkFactory.create( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + } else { + null + } + } + } + } } override fun createDerivationPath(rawPath: String): Network.DerivationPath { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 444a4b5bb6..d785c4e2b6 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,14 +13,14 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber internal class ManagedCryptoCurrencyFactory( diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 05af092bd5..ff1d5fb9ef 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { /** Project - Domain */ implementation(projects.domain.visa) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index f4a46dab61..829d185c39 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -3,12 +3,11 @@ package com.tangem.data.visa.utils import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory -import com.tangem.domain.card.common.util.derivationStyleProvider import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.visa.model.VisaContractInfo import org.joda.time.DateTime import org.joda.time.DateTimeZone @@ -34,7 +33,7 @@ internal class VisaCurrencyFactory @Inject constructor( val currencyNetwork = networkFactory.create( blockchain = Blockchain.Polygon, extraDerivationPath = null, - derivationStyleProvider = userWallet.requireColdWallet().scanResponse.derivationStyleProvider, + derivationStyleProvider = userWallet.derivationStyleProvider, canHandleTokens = true, ) ?: error("Unable to create network for Visa currency") diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index 01e9213ddc..eff1e685f6 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,10 +7,10 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.card.DerivationStyleProvider -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber internal class WalletManagerFactory( diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index fc8569a999..f7867f6b5d 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -11,9 +11,14 @@ android { } dependencies { + implementation(projects.data.common) /** Tangem libraries */ - implementation(tangemDeps.blockchain) // android-library + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) + implementation(projects.libs.tangemSdkApi) + implementation(projects.libs.blockchainSdk) /** Core */ implementation(projects.core.datasource) @@ -21,6 +26,7 @@ dependencies { /** Domain */ implementation(projects.domain.wallets) + implementation(projects.domain.card) api(projects.domain.models) /** Domain models */ @@ -29,15 +35,17 @@ dependencies { /** DI */ implementation(deps.hilt.android) - implementation(project(":domain:legacy")) kapt(deps.hilt.kapt) /** Other deps */ implementation(deps.androidx.datastore) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) /** tests */ testImplementation(projects.domain.models) + testImplementation(projects.common.test) testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt similarity index 56% rename from app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt index b09a27d4cd..e00bde14e5 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt @@ -1,51 +1,50 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.cold import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.TangemSdkError -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.BackendId -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.data.wallets.derivations.Derivations +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.sdk.api.TangemSdkManager -import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber +import javax.inject.Inject -internal typealias Derivations = Map> private typealias DerivedKeys = Map -internal class DefaultDerivationsRepository( +internal class DefaultColdMapDerivationsRepository @Inject constructor( private val tangemSdkManager: TangemSdkManager, - private val userWalletsStore: UserWalletsStore, private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, -) : DerivationsRepository { +) : ColdMapDerivationsRepository { - override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { - derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + override suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + currencies: List, + ): UserWallet.Cold = withContext(dispatchers.io) { + derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network)) } - override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - + override suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Cold, + networkIds: List, + ): UserWallet.Cold = withContext(dispatchers.io) { derivePublicKeysByNetworks( - userWalletId = userWalletId, + userWallet = userWallet, networks = networkIds.mapNotNull { networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, @@ -56,44 +55,55 @@ internal class DefaultDerivationsRepository( ) } - override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { - val userWallet = withContext(dispatchers.io) { - userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - } - - if (userWallet is UserWallet.Hot) { - return - } - - userWallet.requireColdWallet() - + override suspend fun derivePublicKeysByNetworks( + userWallet: UserWallet.Cold, + networks: List, + ): UserWallet.Cold = withContext(dispatchers.io) { if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { Timber.d("Nothing to derive") - return + return@withContext userWallet } - val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse) + val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { Timber.d("Nothing to derive") - return + return@withContext userWallet } - derivePublicKeys(userWalletId = userWalletId, derivations = derivations) + return@withContext derivePublicKeys(userWallet = userWallet, derivations = derivations).first + } + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + derivations: Map>, + ): Pair> = withContext(dispatchers.io) { + // todo replace it in task [REDACTED_JIRA] + val preflightReadFilter = UserWalletIdPreflightReadFilter(userWallet.walletId) + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + preflightReadFilter = preflightReadFilter, + ) + + when (result) { + is CompletionResult.Success -> { + userWallet.updateDerivedKeys(result.data.entries).also { + validateDerivations(scanResponse = it.scanResponse, derivations = derivations) + } to result.data.entries + } + is CompletionResult.Failure -> { + throw result.error + } + } } override suspend fun hasMissedDerivations( - userWalletId: UserWalletId, + userWallet: UserWallet.Cold, networksWithDerivationPath: Map, - ): Boolean { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") - - if (userWallet is UserWallet.Hot) { - return false - } - + ): Boolean = withContext(dispatchers.io) { val derivations = - MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) + MissedDerivationsFinder(userWallet) .findByNetworks( networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) -> networkFactory.create( @@ -104,28 +114,7 @@ internal class DefaultDerivationsRepository( }, ) - return derivations.isNotEmpty() - } - - override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys { - // todo replace it in task [REDACTED_JIRA] - val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId) - tangemSdkManager.derivePublicKeys( - cardId = null, - derivations = derivations, - preflightReadFilter = preflightReadFilter, - ).doOnSuccess { response -> - updatePublicKeys(userWalletId = userWalletId, keys = response.entries) - .doOnSuccess { - // TODO [REDACTED_TASK_KEY] - validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations) - return response.entries - } - .doOnFailure { throw it } - } - .doOnFailure { throw it } - - error("This code should never be reached") + derivations.isNotEmpty() } /** @@ -144,16 +133,7 @@ internal class DefaultDerivationsRepository( } } - private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult { - return withContext(dispatchers.io) { - userWalletsStore.update( - userWalletId = userWalletId, - update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY] - ) - } - } - - private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet { + private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet.Cold { return copy( scanResponse = scanResponse.copy( derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys), diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt new file mode 100644 index 0000000000..dcb1dc574b --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/UserWalletIdPreflightReadFilter.kt @@ -0,0 +1,25 @@ +package com.tangem.data.wallets.cold + +import com.tangem.common.card.Card +import com.tangem.common.core.SessionEnvironment +import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.operations.preflightread.PreflightReadFilter + +/** + * [PreflightReadFilter] for checking if card has expected user wallet id + * +[REDACTED_AUTHOR] + */ +class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWalletId) : PreflightReadFilter { + + override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit + + override fun onFullCardRead(card: Card, environment: SessionEnvironment) { + val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return + + if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound() + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt new file mode 100644 index 0000000000..862b90b035 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -0,0 +1,95 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.CompletionResult +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.map +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultDerivationsRepository @Inject constructor( + private val userWalletsStore: UserWalletsStore, + private val hotDerivationsRepository: HotMapDerivationsRepository, + private val coldDerivationsRepository: ColdMapDerivationsRepository, + private val dispatchers: CoroutineDispatcherProvider, +) : DerivationsRepository { + + override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) + }.also { + userWallet.update(it) + } + } + + override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) + }.also { + userWallet.update(it) + } + } + + override suspend fun derivePublicKeys( + userWalletId: UserWalletId, + derivations: Map>, + ): Map { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + return when (userWallet) { + is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations) + is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations) + }.let { + userWallet.update(it.first) + it.second + } + } + + override suspend fun hasMissedDerivations( + userWalletId: UserWalletId, + networksWithDerivationPath: Map, + ): Boolean { + return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) { + is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) + is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) + } + } + + private suspend fun UserWallet.update(newUserWallet: UserWallet) = withContext(dispatchers.io) { + check(this@update.walletId == newUserWallet.walletId) { + "Cannot update UserWallet with different walletId: ${newUserWallet.walletId}" + } + + if (this@update == newUserWallet) { + return@withContext // No update needed + } + + val updateResult = userWalletsStore.update( + userWalletId = newUserWallet.walletId, + update = { userWalletToUpdate -> newUserWallet }, + ) + + when (updateResult) { + is CompletionResult.Failure -> throw updateResult.error + is CompletionResult.Success -> updateResult.data + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt similarity index 64% rename from app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index 7e740159e5..5ea8f38be1 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain @@ -8,23 +8,26 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.operations.derivation.ExtendedPublicKeysMap +import kotlin.collections.forEach private typealias DerivationData = Pair> +internal typealias Derivations = Map> /** * Finder of missed derivations * - * @property scanResponse scanning response + * @property userWallet User wallet to find derivations for * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { +internal class MissedDerivationsFinder(private val userWallet: UserWallet) { /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { @@ -48,30 +51,39 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } private fun List.mapToNewDerivations(): List { - val config = CardConfig.createConfig(scanResponse.card) + val config = when (userWallet) { + is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card) + is UserWallet.Hot -> Wallet2CardConfig // TODO create config [REDACTED_TASK_KEY] + } return mapNotNull { network -> val blockchain = network.toBlockchain() val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null - findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) + val walletPublicKey = when (userWallet) { + is UserWallet.Cold -> { + val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } + wallet?.publicKey + } + is UserWallet.Hot -> { + val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } + wallet?.publicKey + } + } + + walletPublicKey?.let { + findNewDerivations(curve = curve, publicKey = it, network = network) + } } } - private fun findNewDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - network: Network, - ): DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - val publicKey = wallet.publicKey.toMapKey() - + private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { val derivationCandidates = network .getDerivationCandidates(curve) .ifEmpty { return null } - .filterAlreadyDerivedKeys(publicKey) + .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } - return publicKey to derivationCandidates + return publicKey.toMapKey() to derivationCandidates } private fun Network.getDerivationCandidates(curve: EllipticCurve): List { @@ -88,7 +100,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { return if (getSupportedCurves().contains(curve)) { - derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle()) + derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) } else { null } @@ -118,7 +130,15 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val extendedPublicKeysMap = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) + is UserWallet.Hot -> { + val wallets = userWallet.wallets ?: return emptyList() + wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys + ?: ExtendedPublicKeysMap(emptyMap()) + } + } + return extendedPublicKeysMap.keys.toList() } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index a154dc4f99..5564622241 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -2,14 +2,21 @@ package com.tangem.data.wallets.di import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository import com.tangem.data.wallets.DefaultWalletsRepository +import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository +import com.tangem.data.wallets.derivations.DefaultDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,4 +51,21 @@ internal object WalletsDataModule { fun provideMigrateNamesRepository(appPreferencesStore: AppPreferencesStore): WalletNamesMigrationRepository { return DefaultWalletNamesMigrationRepository(appPreferencesStore) } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletsDataBindsModule { + + @Binds + @Singleton + fun bindDerivationsRepository(impl: DefaultDerivationsRepository): DerivationsRepository + + @Binds + @Singleton + fun bindHotMapDerivationsRepository(impl: DefaultHotMapDerivationsRepository): HotMapDerivationsRepository + + @Binds + @Singleton + fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt new file mode 100644 index 0000000000..3eb9431fc3 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -0,0 +1,139 @@ +package com.tangem.data.wallets.hot + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.wallets.derivations.MissedDerivationsFinder +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.hot.sdk.model.DeriveWalletRequest +import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +internal class DefaultHotMapDerivationsRepository @Inject constructor( + private val networkFactory: NetworkFactory, + private val hotWalletAccessor: HotWalletAccessor, + private val dispatchers: CoroutineDispatcherProvider, +) : HotMapDerivationsRepository { + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + currencies: List, + ): UserWallet.Hot { + return derivePublicKeysByNetworks(userWallet = userWallet, networks = currencies.map(CryptoCurrency::network)) + } + + override suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Hot, + networkIds: List, + ): UserWallet.Hot { + return derivePublicKeysByNetworks( + userWallet = userWallet, + networks = networkIds.mapNotNull { + networkFactory.create( + blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, + extraDerivationPath = null, + userWallet = userWallet, + ) + }, + ) + } + + override suspend fun derivePublicKeysByNetworks( + userWallet: UserWallet.Hot, + networks: List, + ): UserWallet.Hot = withContext(dispatchers.default) { + val derivations = MissedDerivationsFinder(userWallet) + .findByNetworks(networks) + .ifEmpty { + Timber.d("Nothing to derive") + return@withContext userWallet + } + + derivePublicKeys(userWallet, derivations).first + } + + override suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + derivations: Map>, + ): Pair> { + val wallets = userWallet.wallets ?: return userWallet to emptyMap() + + val request = DeriveWalletRequest( + derivations.map { entry -> + val wallet = wallets.first { it.publicKey.contentEquals(entry.key.bytes) } + DeriveWalletRequest.Request( + curve = wallet.curve, + paths = entry.value, + ) + }, + ) + val result = hotWalletAccessor.derivePublicKeys( + hotWalletId = userWallet.hotWalletId, + request = request, + ) + val newKeys = + result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) } + + return userWallet.updateWithNewKeys(newKeys) to newKeys + } + + override suspend fun hasMissedDerivations( + userWallet: UserWallet.Hot, + networksWithDerivationPath: Map, + ): Boolean = withContext(dispatchers.default) { + val derivations = MissedDerivationsFinder(userWallet) + .findByNetworks( + networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) -> + networkFactory.create( + blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null, + extraDerivationPath = extraDerivationPath, + userWallet = userWallet, + ) + }, + ) + + derivations.isNotEmpty() + } + + private fun UserWallet.Hot.updateWithNewKeys(newKeys: Map): UserWallet.Hot { + val wallets = this.wallets ?: return this + val derivedKeys = wallets.associate { + it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys) + } + val updatedKeys = getUpdatedDerivedKeys( + oldKeys = derivedKeys, + newKeys = newKeys, + ) + + return copy( + wallets = wallets.map { wallet -> + wallet.copy( + derivedKeys = updatedKeys[wallet.publicKey.toMapKey()] ?: ExtendedPublicKeysMap(emptyMap()), + ) + }, + ) + } + + private fun getUpdatedDerivedKeys( + oldKeys: Map, + newKeys: Map, + ): Map { + return (oldKeys.keys + newKeys.keys).toSet() + .associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap()) + val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt similarity index 84% rename from app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index a65a2ecd53..9ae11b61c8 100644 --- a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -1,7 +1,7 @@ -package com.tangem.tap.domain.hot +package com.tangem.data.wallets.hot import com.tangem.common.core.TangemSdkError -import com.tangem.features.hotwallet.HotWalletPasswordRequester +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* @@ -12,7 +12,17 @@ class HotWalletAccessor @Inject constructor( private val hotWalletPasswordRequester: HotWalletPasswordRequester, ) { - suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List { + suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = + hotSdkRequest(hotWalletId) { unlock -> + tangemHotSdk.signHashes(unlockHotWallet = unlock, dataToSign = dataToSign) + } + + suspend fun derivePublicKeys(hotWalletId: HotWalletId, request: DeriveWalletRequest): DerivedPublicKeyResponse = + hotSdkRequest(hotWalletId) { unlock -> + tangemHotSdk.derivePublicKey(unlockHotWallet = unlock, request = request) + } + + private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth HotWalletId.AuthType.Password -> requestPassword(false) @@ -20,13 +30,7 @@ class HotWalletAccessor @Inject constructor( } return runCatchingSdkErrors(hotWalletId, auth) { - tangemHotSdk.signHashes( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = it, - ), - dataToSign = dataToSign, - ).also { + block(UnlockHotWallet(hotWalletId, it)).also { hotWalletPasswordRequester.dismiss() } } diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt similarity index 99% rename from app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt rename to data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index ba64c2fda2..b5bfc9da12 100644 --- a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.hot +package com.tangem.data.wallets.hot import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.Wallet diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt similarity index 80% rename from app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt index f5484b4b50..eb812166f7 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import android.annotation.SuppressLint import com.google.common.truth.Truth @@ -7,6 +7,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.configs.GenericCardConfig @@ -14,7 +15,7 @@ import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -27,13 +28,17 @@ import org.junit.Test */ internal class DefaultDerivationsRepositoryTest { - private val tangemSdkManager = mockk() + private val tangemSdkManager = mockk() private val userWalletsStore = mockk() private val repository = DefaultDerivationsRepository( - tangemSdkManager = tangemSdkManager, userWalletsStore = userWalletsStore, dispatchers = TestingCoroutineDispatcherProvider(), - networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), + hotDerivationsRepository = mockk(), + coldDerivationsRepository = DefaultColdMapDerivationsRepository( + tangemSdkManager = tangemSdkManager, + networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), + dispatchers = TestingCoroutineDispatcherProvider(), + ), ) private val defaultUserWalletId = UserWalletId("011") @@ -48,7 +53,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `error if userWalletId not found`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns null + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) @@ -56,7 +61,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -64,13 +69,17 @@ internal class DefaultDerivationsRepositoryTest { @SuppressLint("CheckResult") @Test fun `success if card is not supported derivations`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns defaultUserWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet - runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) + + runCatching { } .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } + .onFailure { + error("Should returns success") + } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -81,13 +90,13 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -102,7 +111,7 @@ internal class DefaultDerivationsRepositoryTest { ), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet runCatching { repository.derivePublicKeys( @@ -113,7 +122,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -123,7 +132,7 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled runCatching { @@ -135,7 +144,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @@ -146,7 +155,7 @@ internal class DefaultDerivationsRepositoryTest { val userWallet = defaultUserWallet.copy( scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) - coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet + coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success( DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), ) @@ -161,7 +170,7 @@ internal class DefaultDerivationsRepositoryTest { .onSuccess { Truth.assertThat(it) } .onFailure { error("Should returns success but $it") } - coVerify(exactly = 1) { userWalletsStore.getSyncOrNull(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) } } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt similarity index 95% rename from app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt index a8398c930c..ae0e28b70b 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DerivedKeysMocks.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DerivedKeysMocks.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationConfigV2 diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt similarity index 90% rename from app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt rename to data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index f623b13c56..426a1bc144 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.data.wallets.derivations import com.google.common.truth.Truth import com.tangem.blockchain.blockchains.cardano.CardanoUtils @@ -13,7 +13,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig -import com.tangem.domain.card.common.util.derivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider import org.junit.Test /** @@ -24,7 +24,8 @@ internal class MissedDerivationsFinderTest { @Test fun `empty derivations for empty currencies`() { val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) - val finder = MissedDerivationsFinder(scanResponse) + val userWallet = MockUserWalletFactory.create(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val actual = finder.find(emptyList()) @@ -36,7 +37,7 @@ internal class MissedDerivationsFinderTest { // Bls is not supported val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf) val actual = finder.find(currencies) @@ -58,7 +59,7 @@ internal class MissedDerivationsFinderTest { ) } val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum val actual = finder.find(currencies) @@ -73,7 +74,7 @@ internal class MissedDerivationsFinderTest { fun `derivations for custom token`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation val actual = finder.find(currencies) @@ -91,7 +92,7 @@ internal class MissedDerivationsFinderTest { fun `derivations for cardano`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) @@ -117,7 +118,7 @@ internal class MissedDerivationsFinderTest { derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf) val actual = finder.find(currencies) @@ -132,7 +133,7 @@ internal class MissedDerivationsFinderTest { derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val userWallet = MockUserWalletFactory.create(scanResponse) - val finder = MissedDerivationsFinder(scanResponse) + val finder = MissedDerivationsFinder(userWallet) val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar val actual = finder.find(currencies) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt index f98a8ed3a7..e8badaf526 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/TangemCardTypesResolver.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Blockchain.Companion.fromId import com.tangem.blockchain.common.Token import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion @@ -76,7 +77,7 @@ internal class TangemCardTypesResolver( } else { return Blockchain.Unknown } - Blockchain.Companion.fromBlockchainName(blockchainName) + Blockchain.fromBlockchainName(blockchainName) } } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt index 6fb8c34eb4..0a6e22e8e5 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/util/ScanResponseExt.kt @@ -6,10 +6,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.card.DerivationStyleProvider import com.tangem.domain.card.TangemCardTypesResolver -import com.tangem.domain.card.TangemDerivationStyleProvider -import com.tangem.domain.card.TangemHotDerivationStyleProvider import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.configs.CardConfig @@ -25,18 +22,6 @@ val ScanResponse.cardTypesResolver: CardTypesResolver walletData = walletData, ) -val UserWallet.derivationStyleProvider: DerivationStyleProvider - get() = when (this) { - is UserWallet.Cold -> this.scanResponse.derivationStyleProvider - is UserWallet.Hot -> TangemHotDerivationStyleProvider() - } - -val ScanResponse.derivationStyleProvider: DerivationStyleProvider - get() = card.derivationStyleProvider - -val CardDTO.derivationStyleProvider: DerivationStyleProvider - get() = TangemDerivationStyleProvider(this) - val UserWallet.Cold.cardTypesResolver: CardTypesResolver get() = scanResponse.cardTypesResolver diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index c60557ebe2..e5fffdace4 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.tokens) implementation(projects.domain.card) + implementation(projects.domain.wallets) implementation(projects.domain.legacy) /* Core */ diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 065f27f412..6c1b21b89b 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.managetokens import arrow.core.Either import arrow.core.flatten -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency @@ -14,6 +13,7 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository @Suppress("LongParameterList") class SaveManagedTokensUseCase( diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index 47e769d11f..3417a876fd 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.markets import arrow.core.Either -import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index ba55e47d78..8b8bda5d78 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries + val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] add derivation config val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt new file mode 100644 index 0000000000..86da09c677 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/ColdMapDerivationsRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +interface ColdMapDerivationsRepository { + + @Throws + suspend fun derivePublicKeys(userWallet: UserWallet.Cold, currencies: List): UserWallet.Cold + + suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Cold, + networkIds: List, + ): UserWallet.Cold + + @Throws + suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Cold, networks: List): UserWallet.Cold + + @Throws + suspend fun derivePublicKeys( + userWallet: UserWallet.Cold, + derivations: Map>, + ): Pair> + + /** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWallet: UserWallet.Cold, + networksWithDerivationPath: Map, + ): Boolean +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt similarity index 92% rename from domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt index 36267e8d35..51e7ee6763 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivationStyleProvider.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.derivations import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.domain.card.common.TapWorkarounds.isWallet2 @@ -25,7 +25,6 @@ internal class TangemDerivationStyleProvider( } } -// TODO remove this class [REDACTED_TASK_KEY] internal class TangemHotDerivationStyleProvider : DerivationStyleProvider { override fun getDerivationStyle(): DerivationStyle? = DerivationStyle.V3 } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt new file mode 100644 index 0000000000..6525c2d528 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationStyleProviderExt.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet + +val UserWallet.derivationStyleProvider: DerivationStyleProvider + get() = when (this) { + is UserWallet.Cold -> scanResponse.derivationStyleProvider + is UserWallet.Hot -> TangemHotDerivationStyleProvider() + } + +val ScanResponse.derivationStyleProvider: DerivationStyleProvider + get() = card.derivationStyleProvider + +val CardDTO.derivationStyleProvider: DerivationStyleProvider + get() = TangemDerivationStyleProvider(this) \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt similarity index 92% rename from domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt index 0f655df789..43b5190947 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt @@ -1,11 +1,11 @@ -package com.tangem.domain.card.repository +package com.tangem.domain.wallets.derivations import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.BackendId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap interface DerivationsRepository { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt new file mode 100644 index 0000000000..65364e9a80 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/HotMapDerivationsRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.wallets.derivations + +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.BackendId +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +interface HotMapDerivationsRepository { + + @Throws + suspend fun derivePublicKeys(userWallet: UserWallet.Hot, currencies: List): UserWallet.Hot + + suspend fun derivePublicKeysByNetworkIds( + userWallet: UserWallet.Hot, + networkIds: List, + ): UserWallet.Hot + + @Throws + suspend fun derivePublicKeysByNetworks(userWallet: UserWallet.Hot, networks: List): UserWallet.Hot + + @Throws + suspend fun derivePublicKeys( + userWallet: UserWallet.Hot, + derivations: Map>, + ): Pair> + + /** Check if user [userWallet] has missed derivations using map of [Network.ID] with extraDerivationPath */ + suspend fun hasMissedDerivations( + userWallet: UserWallet.Hot, + networksWithDerivationPath: Map, + ): Boolean +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt similarity index 91% rename from features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index eac354ece4..6f8391e887 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet +package com.tangem.domain.wallets.hot import com.tangem.hot.sdk.model.HotAuth diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt deleted file mode 100644 index 5616695e03..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.wallets.repository - -import com.tangem.domain.models.network.Network - -interface HotDerivationsRepository { - - fun getAllSupportedNetworks(): Set -} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt similarity index 74% rename from domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt index 86ed75fe6d..164d43a5cf 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/DerivePublicKeysUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DerivePublicKeysUseCase.kt @@ -1,16 +1,17 @@ -package com.tangem.domain.card + +package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.DerivationsRepository class DerivePublicKeysUseCase( private val derivationsRepository: DerivationsRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, currencies: List): Either { - return Either.catch { + return Either.Companion.catch { derivationsRepository.derivePublicKeys(userWalletId, currencies) } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt similarity index 98% rename from domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt index ebbd38f355..e548eef8b7 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.right @@ -10,10 +10,10 @@ import com.tangem.common.extensions.calculateSha256 import com.tangem.crypto.NetworkType import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.operations.derivation.ExtendedPublicKeysMap /** diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt similarity index 75% rename from domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt rename to domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt index 69aa1442c2..c9b008133b 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/HasMissedDerivationsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/HasMissedDerivationsUseCase.kt @@ -1,22 +1,20 @@ -package com.tangem.domain.card +package com.tangem.domain.wallets.usecase -import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.DerivationsRepository + +typealias BackendId = String /** * Use case to check if user has missed derivations * [REDACTED_AUTHOR] */ - -typealias BackendId = String - class HasMissedDerivationsUseCase( private val derivationsRepository: DerivationsRepository, ) { - /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ + /** Check if user [userWalletId] has missed derivations using map of [com.tangem.domain.models.network.Network.ID] with extraDerivationPath */ suspend operator fun invoke( userWalletId: UserWalletId, networksWithDerivationPath: Map, diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts index 310329cc67..a51cd40254 100644 --- a/features/hot-wallet/api/build.gradle.kts +++ b/features/hot-wallet/api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) /* Project - Core */ diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt index 6e6c9c7c8b..49b893e3a6 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester interface HotAccessCodeRequestComponent : ComposableContentComponent, HotWalletPasswordRequester { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index beaba4f29a..f23af23895 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -1,14 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest -import androidx.compose.foundation.focusable import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.FullScreen +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.HotAccessCodeRequestComponent -import com.tangem.features.hotwallet.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 28fb18f009..c34ec66391 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -2,7 +2,7 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.HotWalletPasswordRequester +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt index b1fb298d59..70bb695f83 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt @@ -1,8 +1,8 @@ package com.tangem.features.hotwallet.accesscoderequest.di import com.tangem.core.decompose.model.Model +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.HotAccessCodeRequestComponent -import com.tangem.features.hotwallet.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscoderequest.DefaultHotAccessCodeRequestComponent import com.tangem.features.hotwallet.accesscoderequest.HotAccessCodeRequestModel import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index 29a16c9c2f..5208d91418 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -1,6 +1,6 @@ package com.tangem.features.hotwallet.accesscoderequest.proxy -import com.tangem.features.hotwallet.HotWalletPasswordRequester +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 3fcac8e481..e28cec1763 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.swap.models) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index fdcf5a9d1d..08d5f86574 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -9,12 +9,12 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.CustomTokenFormComponent import com.tangem.features.managetokens.entity.customtoken.ClickableFieldUM diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index ff768a5e76..489033da8d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -17,9 +17,9 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index d6b6ca0899..30f64dad46 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -13,10 +13,10 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.redux.OnboardingManageTokensAction import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.component.OnboardingManageTokensComponent diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index deb96eacdb..e17c925595 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.HasMissedDerivationsUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 3f47acea4b..63b2225169 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -4,7 +4,7 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 083ea5d82b..9520d1847e 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain import arrow.core.getOrElse import com.tangem.common.core.TangemSdkError -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index 4e5166e070..bb4729aac8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain.di import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelComponent -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.ReferralInteractor diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index cb0d326470..33f4a326c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -33,7 +33,6 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency @@ -69,6 +68,7 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ab80da33c7..b6c6a7f60b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -9,7 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase From 154308b973efe71720f467888ca88252c6035449 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 12:42:28 +0300 Subject: [PATCH 009/165] Updated on 2026-08-14 --- .../androidTest/kotlin/com/tangem/tests/DetailsTest.kt | 2 +- .../kotlin/com/tangem/tests/OrganizeTokensTest.kt | 8 ++++---- .../androidTest/kotlin/com/tangem/tests/StoriesTest.kt | 3 +-- app/src/main/assets/tangem-app-config | 2 +- tangem-android-tools | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 1242a1285e..805c4bfe72 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -61,7 +61,7 @@ class DetailsTest : BaseTestCase() { } } - @Test + // @Test fun wallet2DetailsTest() = setupHooks().run { scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2)) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 3856e30249..e64113e05e 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -184,8 +184,8 @@ class OrganizeTokensTest : BaseTestCase() { } step("Check positions of tokens by balance on 'Organize tokens' screen") { onOrganizeTokensScreen { - tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() - tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed() + tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() } } @@ -194,8 +194,8 @@ class OrganizeTokensTest : BaseTestCase() { } step("Check positions of tokens by balance on 'Organize tokens' screen") { onMainScreen { - tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed() - tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 0).assertIsDisplayed() + tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index c17f7fc08d..303ce3f6a0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -8,12 +8,11 @@ import com.tangem.screens.onStoriesScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.intent.KIntent -import org.junit.Test @HiltAndroidTest class StoriesTest : BaseTestCase() { - @Test + // @Test fun clickOnOrderButtonTest() = setupHooks().run { onDisclaimerScreen { diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a4cff08e57..3ac868e93f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a4cff08e572fdd4e51d75a5e1a3deef14cfb1f0b +Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 diff --git a/tangem-android-tools b/tangem-android-tools index 35539d4e63..936cd7232c 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 35539d4e63d1f3ec4ec4f625f7875684ed5033c4 +Subproject commit 936cd7232c2d316c91e2901034417b0b38a63dd1 From e51453c99b1105bbdc39a2d7e2f472407f1c2ae6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 12:50:32 +0300 Subject: [PATCH 010/165] Updated on 2026-08-14 --- .../domain/token/MockCryptoCurrencyFactory.kt | 5 + core/res/src/main/res/values/strings.xml | 9 ++ .../components/inputrow/InputRowRecipient.kt | 51 ++++++- .../data/common/network/NetworkFactory.kt | 8 ++ .../multi/DefaultMultiYieldBalanceFetcher.kt | 4 +- .../tangem/domain/models/network/Network.kt | 14 ++ .../tangem/domain/tokens/mock/MockNetworks.kt | 3 + .../PreviewCustomTokenSelectorComponent.kt | 1 + .../preview/PreviewManageTokensComponent.kt | 1 + .../PreviewOnboardingManageTokensComponent.kt | 1 + .../SendDestinationInitialStateTransformer.kt | 6 +- .../destination/ui/DestinationBlock.kt | 135 +++++++++--------- .../destination/ui/SendDestinationContent.kt | 1 + .../ui/preview/SwapAmountContentPreview.kt | 1 + .../ExpressStatusBottomSheetStateProvider.kt | 1 + 15 files changed, 173 insertions(+), 68 deletions(-) diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index ee2579d3ca..c56e3718ef 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -65,6 +65,10 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = when (blockchain) { + Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS + else -> Network.NameResolvingType.NONE + }, ) return factory.createCoin(network = network) @@ -95,6 +99,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NEVER-MIND", symbol = "NEVER-MIND", diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2ee799d6f3..8623c22b55 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -64,6 +64,10 @@ Add Wallet Select a wallet to log in Welcome back! + + %d card + %d cards + Phone Wallet You successfully backed up your wallet. These words can’t be recovered if lost. Make sure to keep it somewhere secure. @@ -866,6 +870,7 @@ %1$s, %2$s Destination Tag Enter address + Enter ENS name or address Address is the same as wallet address Minimum amount is %s Minimum change is %s @@ -947,11 +952,15 @@ Invalid amount Fee exceeds balance Total amount exceeds balance + Are you sure you want to change the token? After changing, previous data will be reset. + Changing token Swap and send Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive Will be sent a recipient Amount to receive + Are you sure you want to cancel the conversion? After changing, previous data will be reset. + Confirm cancelation Send with swap Transaction sent Prepare to scan card or ring you want to set up. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index b48f8fd849..418341d870 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment.Companion.CenterEnd @@ -19,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.small.TangemIconButton import com.tangem.core.ui.components.fields.SimpleTextField @@ -69,6 +71,7 @@ fun InputRowRecipient( showDivider: Boolean = false, isLoading: Boolean = false, isValuePasted: Boolean = false, + resolvedAddress: String? = null, ) { val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning @@ -144,11 +147,15 @@ fun InputRowRecipient( } else { TangemTheme.colors.text.primary2 }, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8), + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), ) } } + + ResolvedAddressRow( + isLoading = isLoading, + resolvedAddress = resolvedAddress, + ) } } } @@ -203,6 +210,38 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) { } } +@Composable +private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) { + AnimatedContent( + targetState = if (resolvedAddress.isNullOrBlank() || isLoading) { + ResolvedState.Hide + } else { + ResolvedState.Show(resolvedAddress) + }, + label = "Resolved Address", + ) { state -> + if (state is ResolvedState.Show) { + Column { + HorizontalDivider( + thickness = 0.5.dp, + modifier = Modifier.padding(top = 12.dp, bottom = 12.dp), + color = TangemTheme.colors.stroke.primary, + ) + Text( + text = state.address, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } +} + +private sealed interface ResolvedState { + data object Hide : ResolvedState + data class Show(val address: String) : ResolvedState +} + //region preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -224,6 +263,7 @@ private fun InputRowRecipientPreview( onQrCodeClick = {}, modifier = Modifier.background(TangemTheme.colors.background.primary), isRedesignEnabled = false, + resolvedAddress = value.resolvedAddress, ) } } @@ -232,6 +272,7 @@ private data class InputRowRecipientPreviewData( val value: String, val isError: Boolean, val isLoading: Boolean = false, + val resolvedAddress: String? = null, ) private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider { @@ -250,6 +291,12 @@ private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider Network.NameResolvingType.ENS + else -> Network.NameResolvingType.NONE + } + } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun createNetworkStandardType(blockchain: Blockchain) = getNetworkStandardType(blockchain) diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index 2e21a957da..28f27f5139 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -149,7 +149,9 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( // TODO: in the future, consider optimizing this part .chunked(size = 15) // StakeKitApi limitation: no more than 15 requests at the same time .map { - async(dispatchers.io) { stakeKitApi.getMultipleYieldBalances(it).bind() } + async(dispatchers.io) { + stakeKitApi.getMultipleYieldBalances(it).bind() + } } .awaitAll() .flatten() diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt index 95d2ea6ad0..a955b53e54 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt @@ -20,6 +20,7 @@ import kotlinx.serialization.Serializable * currency (for those blockchains that have FeeResource instead of a standard type of fee) * @property canHandleTokens indicates whether the network can handle tokens * @property transactionExtrasType the type of extras supported for sending a transaction + * @property nameResolvingType the type of on-chain name resolution supported by the network (e.g., ENS, SNS etc) */ @Serializable data class Network( @@ -33,6 +34,7 @@ data class Network( val hasFiatFeeRate: Boolean, val canHandleTokens: Boolean, val transactionExtrasType: TransactionExtrasType, + val nameResolvingType: NameResolvingType, ) { /** Raw ID */ @@ -161,4 +163,16 @@ data class Network( -> true } } + + /** + * Represents the type of on-chain name resolution supported by the network. + */ + enum class NameResolvingType { + + /** No name resolution supported */ + NONE, + + /** Ethereum Name Service (ENS) */ + ENS, + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index a717323abe..8f3529b210 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -24,6 +24,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val network2 = Network( @@ -37,6 +38,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val network3 = Network( @@ -50,6 +52,7 @@ internal object MockNetworks { hasFiatFeeRate = true, canHandleTokens = true, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ) val verifiedNetworksStatuses: NonEmptySet diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index 80da5da487..eca3ff5cc5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -65,6 +65,7 @@ internal class PreviewCustomTokenSelectorComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "Network $index", type = "N$index", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 55ac0bba8f..33f07fbef6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -163,6 +163,7 @@ internal class PreviewManageTokensComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NETWORK$networkIndex", type = "N$networkIndex", diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt index bc1276188d..28da1876e7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt @@ -104,6 +104,7 @@ internal class PreviewOnboardingManageTokensComponent( hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "NETWORK$networkIndex", type = "N$networkIndex", diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt index e2cbe95c33..39cb1fd77b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -22,6 +22,10 @@ internal class SendDestinationInitialStateTransformer( Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field } + val placeholder = when (cryptoCurrency.network.nameResolvingType) { + Network.NameResolvingType.NONE -> resourceReference(R.string.send_enter_address_field) + Network.NameResolvingType.ENS -> resourceReference(R.string.send_enter_address_field_ens) + } return DestinationUM.Content( isPrimaryButtonEnabled = false, isInitialized = isInitialized, @@ -32,7 +36,7 @@ internal class SendDestinationInitialStateTransformer( keyboardType = KeyboardType.Text, ), error = null, - placeholder = resourceReference(R.string.send_enter_address_field), + placeholder = placeholder, label = resourceReference(R.string.send_recipient), isValuePasted = false, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index bb84ed1e1e..e1c1b5da9f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -76,11 +76,21 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) .background(TangemTheme.colors.background.tertiary), ) - Text( - text = address.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + val blockchainAddress = address.blockchainAddress + if (!blockchainAddress.isNullOrBlank()) { + Text( + text = blockchainAddress, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } } } @@ -120,12 +130,21 @@ private fun AddressWithMemoBlock( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) { - Text( - modifier = Modifier.weight(1f), - text = address.value, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + val blockchainAddress = address.blockchainAddress + if (!blockchainAddress.isNullOrBlank()) { + Text( + text = blockchainAddress, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } IdentIcon( address = address.value, modifier = Modifier @@ -134,6 +153,7 @@ private fun AddressWithMemoBlock( .background(TangemTheme.colors.background.tertiary), ) } + if (memo != null && memo.value.isNotBlank()) { Text( text = stringResourceSafe(R.string.send_memo, memo.value), @@ -162,64 +182,51 @@ private fun DestinationBlockPreview( } private class DestinationBlockPreviewProvider : PreviewParameterProvider { + val previewItem = DestinationUM.Content( + isPrimaryButtonEnabled = true, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter address"), + label = TextReference.Str("Recipient Address"), + isError = false, + error = null, + isValuePasted = false, + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "Test memo for transaction", + keyboardOptions = KeyboardOptions.Default, + placeholder = TextReference.Str("Enter memo (optional)"), + label = TextReference.Str("Memo"), + isError = false, + error = null, + disabledText = TextReference.Str("Memo disabled"), + isEnabled = true, + isValuePasted = false, + ), + recent = emptyList().toImmutableList(), + wallets = emptyList().toImmutableList(), + networkName = "Ethereum", + isValidating = false, + isInitialized = true, + isRedesignEnabled = false, + ) + override val values: Sequence get() = sequenceOf( - DestinationUM.Content( - isPrimaryButtonEnabled = true, - addressTextField = DestinationTextFieldUM.RecipientAddress( - value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter address"), - label = TextReference.Str("Recipient Address"), - isError = false, - error = null, - isValuePasted = false, + previewItem, + previewItem.copy( + addressTextField = previewItem.addressTextField.copy( + value = "vitalik.eth", + blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", ), - memoTextField = DestinationTextFieldUM.RecipientMemo( - value = "Test memo for transaction", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter memo (optional)"), - label = TextReference.Str("Memo"), - isError = false, - error = null, - disabledText = TextReference.Str("Memo disabled"), - isEnabled = true, - isValuePasted = false, - ), - recent = emptyList().toImmutableList(), - wallets = emptyList().toImmutableList(), - networkName = "Ethereum", - isValidating = false, - isInitialized = true, - isRedesignEnabled = false, ), - DestinationUM.Content( - isPrimaryButtonEnabled = true, - addressTextField = DestinationTextFieldUM.RecipientAddress( - value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter address"), - label = TextReference.Str("Recipient Address"), - isError = false, - error = null, - isValuePasted = false, + previewItem.copy(isRedesignEnabled = true), + previewItem.copy( + addressTextField = previewItem.addressTextField.copy( + value = "vitalik.eth", + blockchainAddress = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", ), - memoTextField = DestinationTextFieldUM.RecipientMemo( - value = "Test memo for transaction", - keyboardOptions = KeyboardOptions.Default, - placeholder = TextReference.Str("Enter memo (optional)"), - label = TextReference.Str("Memo"), - isError = false, - error = null, - disabledText = TextReference.Str("Memo disabled"), - isEnabled = true, - isValuePasted = false, - ), - recent = emptyList().toImmutableList(), - wallets = emptyList().toImmutableList(), - networkName = "Ethereum", - isValidating = false, - isInitialized = true, isRedesignEnabled = true, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 31966f7310..da247fa248 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -142,6 +142,7 @@ private fun LazyListScope.addressItem( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ), + resolvedAddress = address.blockchainAddress, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index b40ecc6c80..d7fd561378 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -39,6 +39,7 @@ internal data object SwapAmountContentPreview { hasFiatFeeRate = false, canHandleTokens = false, transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, ), name = "Bitcoin", diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt index 8e123b32df..fc4a214ee0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt @@ -107,5 +107,6 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider Date: Sun, 27 Jul 2025 20:47:11 +0500 Subject: [PATCH 011/165] Updated on 2026-08-14 --- .../hotwallet/accesscode/Constants.kt | 3 + .../confirm/ConfirmAccessCodeComponent.kt | 52 ++++++++++++ .../confirm/ConfirmAccessCodeModel.kt | 45 ++++++++++ .../confirm/entity/ConfirmAccessCodeUM.kt | 12 +++ .../confirm/ui/ConfirmAccessCodeContent.kt | 46 ++++++++++ .../di/AccessCodeModule.kt} | 12 ++- .../set}/SetAccessCodeComponent.kt | 24 ++++-- .../accesscode/set/SetAccessCodeModel.kt | 45 ++++++++++ .../accesscode/set/entity/SetAccessCodeUM.kt | 12 +++ .../accesscode/set/ui/SetAccessCodeContent.kt | 46 ++++++++++ .../ui/AccessCodeEnter.kt} | 55 ++++-------- .../{root => entry}/AddExistingWalletModel.kt | 36 ++++---- .../DefaultAddExistingWalletComponent.kt | 0 .../di/AddExistingWalletModule.kt | 0 .../entity/AddExistingWalletUM.kt | 0 .../routing/AddExistingWalletChildFactory.kt | 18 +++- .../routing/AddExistingWalletRoute.kt | 5 +- .../ui/AddExistingWalletContent.kt | 0 .../setaccesscode/SetAccessCodeModel.kt | 84 ------------------- .../setaccesscode/entity/SetAccessCodeUM.kt | 22 ----- .../setaccesscode/ui/SetAccessCodeContent.kt | 59 ------------- 21 files changed, 343 insertions(+), 233 deletions(-) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode/di/SetAccessCodeModule.kt => accesscode/di/AccessCodeModule.kt} (52%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode/set}/SetAccessCodeComponent.kt (65%) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode/ui/SetAccessCodeEnter.kt => accesscode/ui/AccessCodeEnter.kt} (66%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/AddExistingWalletModel.kt (75%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/DefaultAddExistingWalletComponent.kt (100%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/di/AddExistingWalletModule.kt (100%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/entity/AddExistingWalletUM.kt (100%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/routing/AddExistingWalletChildFactory.kt (74%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/routing/AddExistingWalletRoute.kt (79%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/{root => entry}/ui/AddExistingWalletContent.kt (100%) delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt new file mode 100644 index 0000000000..ca5ecaa367 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt @@ -0,0 +1,3 @@ +package com.tangem.features.hotwallet.accesscode + +const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt new file mode 100644 index 0000000000..849eb2cb57 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt @@ -0,0 +1,52 @@ +package com.tangem.features.hotwallet.accesscode.confirm + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.features.hotwallet.accesscode.confirm.ui.SetAccessCodeConfirmContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class ConfirmAccessCodeComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + + private val model: ConfirmAccessCodeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + DisableScreenshotsDisposableEffect() + + SetAccessCodeConfirmContent( + accessCode = state.accessCode, + onAccessCodeChange = state.onAccessCodeChange, + accessCodeLength = state.accessCodeLength, + onConfirm = state.onConfirm, + buttonEnabled = state.buttonEnabled, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onAccessCodeConfirmed() + } + + data class Params( + val accessCodeToConfirm: String, + val callbacks: ModelCallbacks, + ) + + @AssistedFactory + interface Factory { + fun create(context: AppComponentContext, params: Params): ConfirmAccessCodeComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt new file mode 100644 index 0000000000..25a77bdbdc --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt @@ -0,0 +1,45 @@ +package com.tangem.features.hotwallet.accesscode.confirm + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.accesscode.confirm.entity.ConfirmAccessCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@Stable +@ModelScoped +internal class ConfirmAccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun getInitialState() = ConfirmAccessCodeUM( + accessCode = "", + onAccessCodeChange = ::onAccessCodeChange, + buttonEnabled = false, + onConfirm = ::onConfirm, + ) + + private fun onAccessCodeChange(value: String) { + uiState.update { + it.copy( + accessCode = value, + buttonEnabled = value == params.accessCodeToConfirm, + ) + } + } + + private fun onConfirm() { + params.callbacks.onAccessCodeConfirmed() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt new file mode 100644 index 0000000000..f2df925578 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.hotwallet.accesscode.confirm.entity + +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH + +internal data class ConfirmAccessCodeUM( + val accessCode: String, + val onAccessCodeChange: (String) -> Unit, + val buttonEnabled: Boolean, + val onConfirm: () -> Unit, +) { + val accessCodeLength: Int = ACCESS_CODE_LENGTH +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt new file mode 100644 index 0000000000..d85e2ea0d1 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.hotwallet.accesscode.confirm.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.features.hotwallet.accesscode.ui.AccessCodeEnter +import com.tangem.core.res.R + +@Composable +internal fun SetAccessCodeConfirmContent( + accessCode: String, + onAccessCodeChange: (String) -> Unit, + accessCodeLength: Int, + onConfirm: () -> Unit, + buttonEnabled: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + AccessCodeEnter( + modifier = Modifier + .padding(top = 16.dp) + .weight(1f), + accessCode = accessCode, + onAccessCodeChange = onAccessCodeChange, + accessCodeLength = accessCodeLength, + reEnterAccessCodeState = true, + ) + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + text = stringResourceSafe(R.string.common_confirm), + onClick = onConfirm, + enabled = buttonEnabled, + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt similarity index 52% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt index fccaaac528..8c79d221da 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/SetAccessCodeModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt @@ -1,7 +1,8 @@ -package com.tangem.features.hotwallet.setaccesscode.di +package com.tangem.features.hotwallet.accesscode.di import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel +import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeModel +import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,10 +12,15 @@ import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) -internal interface SetAccessCodeModule { +internal interface AccessCodeModule { @Binds @IntoMap @ClassKey(SetAccessCodeModel::class) fun bindSetAccessCodeModel(model: SetAccessCodeModel): Model + + @Binds + @IntoMap + @ClassKey(ConfirmAccessCodeModel::class) + fun bindConfirmAccessCodeModel(model: ConfirmAccessCodeModel): Model } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt similarity index 65% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt index b1f774aa93..c8a03346c0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode +package com.tangem.features.hotwallet.accesscode.set import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,8 +8,9 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent +import com.tangem.features.hotwallet.accesscode.set.ui.SetAccessCodeContent import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject internal class SetAccessCodeComponent @AssistedInject constructor( @@ -23,21 +24,28 @@ internal class SetAccessCodeComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + DisableScreenshotsDisposableEffect() + SetAccessCodeContent( - state = state, - onBack = { model.onBack() }, + accessCode = state.accessCode, + onAccessCodeChange = state.onAccessCodeChange, + accessCodeLength = state.accessCodeLength, + onContinue = state.onContinue, + buttonEnabled = state.buttonEnabled, modifier = modifier, ) - - DisableScreenshotsDisposableEffect() } interface ModelCallbacks { - fun onBackClick() - fun onAccessCodeSet() + fun onAccessCodeSet(accessCode: String) } data class Params( val callbacks: ModelCallbacks, ) + + @AssistedFactory + interface Factory { + fun create(context: AppComponentContext, params: Params): SetAccessCodeComponent + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt new file mode 100644 index 0000000000..4f57d50886 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt @@ -0,0 +1,45 @@ +package com.tangem.features.hotwallet.accesscode.set + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.accesscode.set.entity.SetAccessCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@Stable +@ModelScoped +internal class SetAccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + private fun getInitialState() = SetAccessCodeUM( + accessCode = "", + onAccessCodeChange = ::onAccessCodeChange, + buttonEnabled = false, + onContinue = ::onContinue, + ) + + private fun onAccessCodeChange(value: String) { + uiState.update { + it.copy( + accessCode = value, + buttonEnabled = value.length == uiState.value.accessCodeLength, + ) + } + } + + private fun onContinue() { + params.callbacks.onAccessCodeSet(uiState.value.accessCode) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt new file mode 100644 index 0000000000..68fa89c2fe --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.hotwallet.accesscode.set.entity + +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH + +internal data class SetAccessCodeUM( + val accessCode: String, + val onAccessCodeChange: (String) -> Unit, + val buttonEnabled: Boolean, + val onContinue: () -> Unit, +) { + val accessCodeLength: Int = ACCESS_CODE_LENGTH +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt new file mode 100644 index 0000000000..0fcf53c2df --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.hotwallet.accesscode.set.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.features.hotwallet.accesscode.ui.AccessCodeEnter +import com.tangem.core.res.R + +@Composable +internal fun SetAccessCodeContent( + accessCode: String, + onAccessCodeChange: (String) -> Unit, + accessCodeLength: Int, + onContinue: () -> Unit, + buttonEnabled: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + AccessCodeEnter( + modifier = Modifier + .padding(top = 16.dp) + .weight(1f), + accessCode = accessCode, + onAccessCodeChange = onAccessCodeChange, + accessCodeLength = accessCodeLength, + reEnterAccessCodeState = false, + ) + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + text = stringResourceSafe(R.string.common_continue), + onClick = onContinue, + enabled = buttonEnabled, + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt similarity index 66% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt index bd787c1822..ba80fd0bb6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode.ui +package com.tangem.features.hotwallet.accesscode.ui import android.content.res.Configuration import androidx.compose.foundation.background @@ -15,11 +15,12 @@ import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM @Composable -internal fun SetAccessCodeEnter( - state: SetAccessCodeUM, +internal fun AccessCodeEnter( + accessCode: String, + onAccessCodeChange: (String) -> Unit, + accessCodeLength: Int, reEnterAccessCodeState: Boolean, modifier: Modifier = Modifier, ) { @@ -52,7 +53,7 @@ internal fun SetAccessCodeEnter( } else { stringResourceSafe( R.string.access_code_create_description, - state.accessCodeLength, + accessCodeLength, ) }, style = TangemTheme.typography.body1, @@ -67,18 +68,10 @@ internal fun SetAccessCodeEnter( contentAlignment = Alignment.Center, ) { PinTextField( - length = state.accessCodeLength, + length = accessCodeLength, isPasswordVisual = true, - value = if (reEnterAccessCodeState) { - state.accessCodeSecond - } else { - state.accessCodeFirst - }, - onValueChange = if (reEnterAccessCodeState) { - state.onAccessCodeSecondChange - } else { - state.onAccessCodeFirstChange - }, + value = accessCode, + onValueChange = onAccessCodeChange, ) } } @@ -89,37 +82,25 @@ internal fun SetAccessCodeEnter( @Composable private fun Preview() { TangemThemePreview { - SetAccessCodeEnter( + AccessCodeEnter( + accessCode = "123456", + onAccessCodeChange = {}, + accessCodeLength = 6, reEnterAccessCodeState = false, - state = SetAccessCodeUM( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = {}, - onAccessCodeSecondChange = {}, - buttonEnabled = false, - onContinue = {}, - ), ) } } -@Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable private fun Preview2() { TangemThemePreview { - SetAccessCodeEnter( + AccessCodeEnter( + accessCode = "123456", + onAccessCodeChange = {}, + accessCodeLength = 6, reEnterAccessCodeState = true, - state = SetAccessCodeUM( - step = SetAccessCodeUM.Step.ConfirmAccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = {}, - onAccessCodeSecondChange = {}, - buttonEnabled = false, - onContinue = {}, - ), ) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt similarity index 75% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index 92b04ffe28..c021c4b032 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -9,11 +9,12 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.domain.settings.ShouldAskPermissionUseCase -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent -import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent +import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @@ -31,20 +32,22 @@ internal class AddExistingWalletModel @Inject constructor( val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks() - val accessCodeModelCallbacks = AccessCodeModelCallbacks() val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks() + val setAccessCodeModelCallbacks = SetAccessCodeModelCallbacks() + val confirmAccessCodeModelCallbacks = ConfirmAccessCodeModelCallbacks() val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks() val stackNavigation = StackNavigation() fun onChildBack(currentRoute: AddExistingWalletRoute) { when (currentRoute) { - AddExistingWalletRoute.Import -> stackNavigation.pop() - AddExistingWalletRoute.BackupCompleted -> Unit - AddExistingWalletRoute.AccessCode -> stackNavigation.pop() - AddExistingWalletRoute.PushNotifications -> Unit - AddExistingWalletRoute.SetupFinished -> Unit - AddExistingWalletRoute.Start -> Unit + is AddExistingWalletRoute.Import -> stackNavigation.pop() + is AddExistingWalletRoute.BackupCompleted -> Unit + is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop() + is AddExistingWalletRoute.SetAccessCode -> stackNavigation.pop() + is AddExistingWalletRoute.PushNotifications -> Unit + is AddExistingWalletRoute.SetupFinished -> Unit + is AddExistingWalletRoute.Start -> Unit } } @@ -66,16 +69,18 @@ internal class AddExistingWalletModel @Inject constructor( inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { override fun onContinueClick() { - stackNavigation.push(AddExistingWalletRoute.AccessCode) + stackNavigation.push(AddExistingWalletRoute.SetAccessCode) } } - inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks { - override fun onBackClick() { - stackNavigation.pop() + inner class SetAccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks { + override fun onAccessCodeSet(accessCode: String) { + stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(accessCode)) } + } - override fun onAccessCodeSet() { + inner class ConfirmAccessCodeModelCallbacks : ConfirmAccessCodeComponent.ModelCallbacks { + override fun onAccessCodeConfirmed() { modelScope.launch { val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) if (shouldRequestPush) { @@ -95,7 +100,8 @@ internal class AddExistingWalletModel @Inject constructor( } } - inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { + inner class MobileWalletSetupFinishedComponentModelCallbacks : + MobileWalletSetupFinishedComponent.ModelCallbacks { override fun onContinueClick() { router.replaceAll(AppRoute.Wallet) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt similarity index 100% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/DefaultAddExistingWalletComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt similarity index 100% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt similarity index 100% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/entity/AddExistingWalletUM.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt similarity index 74% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index e4926281e5..38642f07bf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -2,7 +2,8 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent +import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent +import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -13,6 +14,8 @@ import javax.inject.Inject internal class AddExistingWalletChildFactory @Inject constructor( private val pushNotificationsComponent: PushNotificationsComponent.Factory, + private val setAccessCodeSetComponentFactory: SetAccessCodeComponent.Factory, + private val confirmAccessCodeComponentFactory: ConfirmAccessCodeComponent.Factory, ) { fun createChild( @@ -39,10 +42,17 @@ internal class AddExistingWalletChildFactory @Inject constructor( callbacks = model.manualBackupCompletedComponentModelCallbacks, ), ) - is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent( + is AddExistingWalletRoute.SetAccessCode -> setAccessCodeSetComponentFactory.create( context = childContext, params = SetAccessCodeComponent.Params( - callbacks = model.accessCodeModelCallbacks, + callbacks = model.setAccessCodeModelCallbacks, + ), + ) + is AddExistingWalletRoute.ConfirmAccessCode -> confirmAccessCodeComponentFactory.create( + context = childContext, + params = ConfirmAccessCodeComponent.Params( + accessCodeToConfirm = route.accessCode, + callbacks = model.confirmAccessCodeModelCallbacks, ), ) is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create( @@ -51,7 +61,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( callbacks = model.pushNotificationsComponentModelCallbacks, ), ) - AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( + is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( context = childContext, params = MobileWalletSetupFinishedComponent.Params( callbacks = model.mobileWalletSetupFinishedComponentModelCallbacks, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt similarity index 79% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt index c08f900605..f4f83e4193 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt @@ -15,7 +15,10 @@ internal sealed class AddExistingWalletRoute : Route { object BackupCompleted : AddExistingWalletRoute() @Serializable - object AccessCode : AddExistingWalletRoute() + object SetAccessCode : AddExistingWalletRoute() + + @Serializable + data class ConfirmAccessCode(val accessCode: String) : AddExistingWalletRoute() @Serializable object PushNotifications : AddExistingWalletRoute() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt similarity index 100% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/ui/AddExistingWalletContent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt deleted file mode 100644 index 945ff9ad20..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -@Stable -@ModelScoped -internal class SetAccessCodeModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - internal val uiState: StateFlow - field = MutableStateFlow(getInitialState()) - - fun onBack() { - when (uiState.value.step) { - SetAccessCodeUM.Step.AccessCode -> { - params.callbacks.onBackClick() - } - SetAccessCodeUM.Step.ConfirmAccessCode -> { - uiState.update { - it.copy( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeSecond = "", - ) - } - } - } - } - - private fun getInitialState() = SetAccessCodeUM( - step = SetAccessCodeUM.Step.AccessCode, - accessCodeFirst = "", - accessCodeSecond = "", - onAccessCodeFirstChange = ::onAccessCodeFirstChange, - onAccessCodeSecondChange = ::onAccessCodeSecondChange, - buttonEnabled = false, - onContinue = ::onContinue, - ) - - private fun onAccessCodeFirstChange(value: String) { - uiState.update { - it.copy( - accessCodeFirst = value, - buttonEnabled = value.length == uiState.value.accessCodeLength, - ) - } - } - - private fun onAccessCodeSecondChange(value: String) { - uiState.update { - it.copy( - accessCodeSecond = value, - buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond, - ) - } - } - - private fun onContinue() { - when (uiState.value.step) { - SetAccessCodeUM.Step.AccessCode -> { - uiState.update { - it.copy( - step = SetAccessCodeUM.Step.ConfirmAccessCode, - ) - } - } - SetAccessCodeUM.Step.ConfirmAccessCode -> { - params.callbacks.onAccessCodeSet() - } - } - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt deleted file mode 100644 index a5e98ca830..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.entity - -internal data class SetAccessCodeUM( - val step: Step, - val accessCodeFirst: String, - val accessCodeSecond: String, - val onAccessCodeFirstChange: (String) -> Unit, - val onAccessCodeSecondChange: (String) -> Unit, - val buttonEnabled: Boolean, - val onContinue: () -> Unit, -) { - val accessCodeLength: Int = ACCESS_CODE_LENGTH - - enum class Step { - AccessCode, - ConfirmAccessCode, - } - - companion object { - private const val ACCESS_CODE_LENGTH = 6 - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt deleted file mode 100644 index 64449365ae..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.ui - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemAnimations -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM -import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.* -import com.tangem.core.res.R - -@Composable -internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) { - BackHandler(onBack = onBack) - - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - ) { - AnimatedContent( - modifier = Modifier.weight(1f), - targetState = state.step, - transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> target.ordinal > initial.ordinal }, - label = "AnimatedContent", - ) { step -> - when (step) { - AccessCode -> SetAccessCodeEnter( - modifier = Modifier.padding(top = 16.dp), - state = state, - reEnterAccessCodeState = false, - ) - ConfirmAccessCode -> SetAccessCodeEnter( - modifier = Modifier.padding(top = 16.dp), - state = state, - reEnterAccessCodeState = true, - ) - } - } - - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .imePadding(), - text = if (state.step == ConfirmAccessCode) { - stringResourceSafe(R.string.common_confirm) - } else { - stringResourceSafe(R.string.common_continue) - }, - onClick = state.onContinue, - ) - } -} \ No newline at end of file From c89a95265d36b41c3299f19477bbec71e4da08d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 27 Jul 2025 22:16:37 +0500 Subject: [PATCH 012/165] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 4 +- .../components/appbar/AppBarWithBackButton.kt | 4 +- .../appbar/AppBarWithBackButtonAndIcon.kt | 8 ++-- .../ui/components/appbar/TangemTopAppBar.kt | 24 +++++----- .../ui/components/appbar/TopAppBarButton.kt | 44 ++++++++++++++----- .../appbar/models/TopAppBarButtonUM.kt | 30 ++++++++++--- .../sheet/TangemBottomSheetTitle.kt | 4 +- .../disclaimer/impl/ui/DisclaimerScreen.kt | 5 +-- .../preview/PreviewManageTokensComponent.kt | 4 +- .../managetokens/model/ManageTokensModel.kt | 4 +- .../features/nft/details/ui/NFTDetails.kt | 5 +-- .../features/nft/receive/ui/NFTReceive.kt | 5 +-- .../features/nft/traits/ui/NFTAssetTraits.kt | 5 +-- .../v2/stepper/impl/ui/OnboardingStepper.kt | 2 +- .../main/entity/OnrampMainComponentUM.kt | 9 ++-- .../main/entity/factory/OnrampStateFactory.kt | 11 ++++- .../redirect/model/OnrampRedirectModel.kt | 5 +-- .../ui/OnrampSuccessComponentContent.kt | 5 +-- .../presentation/QrScanningContent.kt | 12 ++--- .../ExcludedBlockchainsScreen.kt | 4 +- .../connections/model/WcConnectionsModel.kt | 5 +-- .../connections/ui/WcConnectionsContent.kt | 12 +++-- .../ui/preview/WcConnectionsPreviewData.kt | 5 +-- 23 files changed, 126 insertions(+), 90 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 9476ea0cad..4815b47788 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -43,9 +43,9 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { config = config, containerColor = TangemTheme.colors.background.secondary, titleText = resourceReference(R.string.give_permission_title), - titleAction = TopAppBarButtonUM( + titleAction = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_information_24, - onIconClicked = { isPermissionAlertShow = true }, + onClicked = { isPermissionAlertShow = true }, ), content = { content: GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheetContent(content = content) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index bd95803a6a..4c1b0f0e4f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -32,9 +32,9 @@ fun AppBarWithBackButton( TangemTopAppBar( modifier = modifier, title = text, - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = iconRes ?: R.drawable.ic_back_24, - onIconClicked = onBackClick, + onClicked = onBackClick, ), containerColor = containerColor, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index aaa6fcf804..47544cffc6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -27,14 +27,14 @@ fun AppBarWithBackButtonAndIcon( title = text, subtitle = subtitle, containerColor = backgroundColor, - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = backIconRes ?: R.drawable.ic_back_24, - onIconClicked = onBackClick, + onClicked = onBackClick, ), endButton = if (iconRes != null && onIconClick != null) { - TopAppBarButtonUM( + TopAppBarButtonUM.Icon( iconRes = iconRes, - onIconClicked = onIconClick, + onClicked = onIconClick, ) } else { null diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt index 319beeb81f..c963651693 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt @@ -301,25 +301,25 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider { + IconButton( + enabled = button.enabled, + modifier = modifier.size(TangemTheme.dimens.size32), + onClick = button.onClicked, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = button.iconRes), + tint = tint, + contentDescription = null, + ) + } + } + is TopAppBarButtonUM.Text -> { + Text( + modifier = modifier + .conditional(button.enabled) { + clickable { button.onClicked() } + } + .padding(4.dp), + text = button.text.resolveReference(), + color = tint, + style = TangemTheme.typography.body1, + ) + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt index 7104e25cf9..aa06b614a0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt @@ -2,21 +2,39 @@ package com.tangem.core.ui.components.appbar.models import androidx.annotation.DrawableRes import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference -data class TopAppBarButtonUM( - @DrawableRes val iconRes: Int, - val onIconClicked: () -> Unit, - val enabled: Boolean = true, +sealed class TopAppBarButtonUM( + open val onClicked: () -> Unit, + open val enabled: Boolean = true, ) { + data class Icon( + @DrawableRes val iconRes: Int, + override val onClicked: () -> Unit, + override val enabled: Boolean = true, + ) : TopAppBarButtonUM(onClicked, enabled) + + data class Text( + val text: TextReference, + override val onClicked: () -> Unit, + override val enabled: Boolean = true, + ) : TopAppBarButtonUM(onClicked, enabled) + @Suppress("FunctionName") companion object { fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked) - fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM( + fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = Icon( iconRes = R.drawable.ic_back_24, - onIconClicked = onBackClicked, + onClicked = onBackClicked, + enabled = enabled, + ) + + fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text( + text = text, + onClicked = onTextClicked, enabled = enabled, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt index 8ca60f6218..9003e830c0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt @@ -57,9 +57,9 @@ private fun Preview_TangemBottomSheetTitle() { TangemThemePreview { TangemBottomSheetTitle( title = "Title", - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_information_24, - onIconClicked = {}, + onClicked = {}, ), containerColor = TangemTheme.colors.background.secondary, ) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 72c38a4347..4a68dba57e 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -66,9 +66,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { ) { TangemTopAppBar( title = resourceReference(R.string.disclaimer_title), - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.popBack, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.popBack, ).takeIf { state.isTosAccepted }, titleAlignment = Alignment.CenterHorizontally, textColor = textColor, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 33f07fbef6..d7caaf672c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -42,9 +42,9 @@ internal class PreviewManageTokensComponent( ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = {}, - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_plus_24, - onIconClicked = {}, + onClicked = {}, ), ) } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 489033da8d..91e1814599 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -129,9 +129,9 @@ internal class ManageTokensModel @Inject constructor( topBar = ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = router::pop, - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_plus_24, - onIconClicked = ::navigateToAddCustomToken, + onClicked = ::navigateToAddCustomToken, ), ), search = SearchBarUM( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 2c3ad2ede5..45319ee1ba 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -28,9 +28,8 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = state.nftAsset.name, ) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 21c147c3a9..dd9cfc1aa7 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -28,9 +28,8 @@ internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = stringResourceSafe(id = R.string.nft_receive_title), subtitle = state.appBarSubtitle.resolveReference(), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt index 9f18df108f..f1ebd94f83 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt @@ -22,9 +22,8 @@ internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifi ) { TangemTopAppBar( modifier = Modifier, - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = state.onBackClick, ), title = stringResourceSafe(R.string.nft_traits_title), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt index 50136e9d10..e3e01fe44d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt @@ -49,7 +49,7 @@ internal fun OnboardingStepper( ) { TangemTopAppBar( startButton = TopAppBarButtonUM.Back(onBackClick), - endButton = TopAppBarButtonUM(iconRes = R.drawable.ic_chat_24, onIconClicked = onSupportButtonClick) + endButton = TopAppBarButtonUM.Icon(iconRes = R.drawable.ic_chat_24, onClicked = onSupportButtonClick) .takeIf { state.steps != state.currentStep }, title = if (state.steps == state.currentStep) { resourceReference(R.string.common_done) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 4015d007a8..ae9041847c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -24,14 +24,13 @@ internal sealed interface OnrampMainComponentUM { ) : OnrampMainComponentUM { override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = onClose, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = onClose, enabled = true, ), - endButtonUM = TopAppBarButtonUM( + endButtonUM = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = openSettings, + onClicked = openSettings, enabled = false, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 0fbbcbe0ca..8eb611e8fe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency @@ -37,7 +38,10 @@ internal class OnrampStateFactory( fun getReadyState(currency: OnrampCurrency): OnrampMainComponentUM.Content { val state = currentStateProvider() - val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } return OnrampMainComponentUM.Content( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), buyButtonConfig = state.buyButtonConfig, @@ -80,7 +84,10 @@ internal class OnrampStateFactory( fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() - val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) + val endButton = when (val button = state.topBarConfig.endButtonUM) { + is TopAppBarButtonUM.Icon -> button.copy(enabled = true) + is TopAppBarButtonUM.Text -> button.copy(enabled = true) + } return when (state) { is OnrampMainComponentUM.Content -> state.copy( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index ce08252b7b..d19b0fc95b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -54,9 +54,8 @@ internal class OnrampRedirectModel @Inject constructor( resourceReference(R.string.common_buy), stringReference(" ${params.cryptoCurrency.name}"), ), - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = appRouter::pop, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = appRouter::pop, enabled = true, ), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt index 39ea071d73..36a1e410e8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/ui/OnrampSuccessComponentContent.kt @@ -61,9 +61,8 @@ private fun Content(state: OnrampSuccessComponentUM.Content, onBackClick: () -> .systemBarsPadding(), topBar = { TangemTopAppBar( - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = onBackClick, + startButton = TopAppBarButtonUM.Back( + onBackClicked = onBackClick, ), ) }, diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index b26bb96aa2..7e11df9de8 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -66,9 +66,9 @@ internal fun QrScanningContent( TangemTopAppBar( modifier = Modifier.statusBarsPadding(), title = uiState.topBarConfig.title?.resolveReference(), - startButton = TopAppBarButtonUM( + startButton = TopAppBarButtonUM.Icon( iconRes = uiState.topBarConfig.startIcon, - onIconClicked = uiState.onBackClick, + onClicked = uiState.onBackClick, ), textColor = TangemTheme.colors.text.constantWhite, iconTint = TangemColorPalette.White, @@ -80,9 +80,9 @@ internal fun QrScanningContent( label = "Flash Change", ) { TopAppBarButton( - button = TopAppBarButtonUM( + button = TopAppBarButtonUM.Icon( iconRes = if (it) R.drawable.ic_flash_on_24 else R.drawable.ic_flash_off_24, - onIconClicked = { + onClicked = { isFlash = !isFlash }, ), @@ -91,9 +91,9 @@ internal fun QrScanningContent( } TopAppBarButton( - button = TopAppBarButtonUM( + button = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_gallery_24, - onIconClicked = uiState.onGalleryClick, + onClicked = uiState.onGalleryClick, ), tint = TangemColorPalette.White, ) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt index 772c22afab..277ab7fb86 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt @@ -46,9 +46,9 @@ internal fun ExcludedBlockchainsScreen(state: ExcludedBlockchainsScreenUM, modif TangemTopAppBar( title = resourceReference(R.string.excluded_blockchains), startButton = TopAppBarButtonUM.Back(onBackClicked = state.popBack), - endButton = TopAppBarButtonUM( + endButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_refresh_24, - onIconClicked = state.onRecoverClick, + onClicked = state.onRecoverClick, ), ) }, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index f39cba49ce..9efea80a5b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -134,9 +134,8 @@ internal class WcConnectionsModel @Inject constructor( private fun getInitialState(): WcConnectionsState { return WcConnectionsState( topAppBarConfig = WcConnectionsTopAppBarConfig( - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = router::pop, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = router::pop, enabled = true, ), disconnectAllItem = TangemDropdownMenuItem( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index 0e9068267d..b0a695365f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TopAppBarButton import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.components.snackbar.TangemSnackbarHost @@ -251,13 +252,10 @@ private fun ConnectionsTopBar( actionIconContentColor = TangemTheme.colors.icon.primary1, ), navigationIcon = { - IconButton(onClick = config.startButtonUM.onIconClicked) { - Icon( - painter = painterResource(id = config.startButtonUM.iconRes), - tint = TangemTheme.colors.icon.primary1, - contentDescription = "Back", - ) - } + TopAppBarButton( + button = config.startButtonUM, + tint = TangemTheme.colors.icon.primary1, + ) }, title = { Text( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index 62530a542a..d2cbeb2a8c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -118,9 +118,8 @@ internal object WcConnectionsPreviewData { ) val stateWithEmptyConnections = WcConnectionsState( topAppBarConfig = WcConnectionsTopAppBarConfig( - startButtonUM = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = {}, + startButtonUM = TopAppBarButtonUM.Back( + onBackClicked = {}, enabled = true, ), disconnectAllItem = TangemDropdownMenuItem( From c761f40891d238608da19ca9e69e4a94c286326e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Jul 2025 13:57:47 +0500 Subject: [PATCH 013/165] Updated on 2026-08-14 --- .../entry/AddExistingWalletModel.kt | 37 +++--- .../AddExistingWalletStepperStateManager.kt | 79 +++++++++++++ .../DefaultAddExistingWalletComponent.kt | 43 ++++++- .../entry/di/AddExistingWalletModule.kt | 32 +++++- .../entry/entity/AddExistingWalletUM.kt | 2 +- .../routing/AddExistingWalletChildFactory.kt | 4 +- .../entry/routing/AddExistingWalletRoute.kt | 2 +- .../entry/ui/AddExistingWalletContent.kt | 25 +++-- .../stepper/api/HotWalletStepperComponent.kt | 33 ++++++ .../stepper/di/HotWalletStepperModule.kt | 18 +++ .../impl/DefaultHotWalletStepperComponent.kt | 48 ++++++++ .../stepper/impl/HotWalletStepperModel.kt | 42 +++++++ .../stepper/impl/ui/HotWalletStepper.kt | 106 ++++++++++++++++++ 13 files changed, 437 insertions(+), 34 deletions(-) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index c021c4b032..ad499a7aa4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -1,8 +1,9 @@ -package com.tangem.features.hotwallet.addexistingwallet.root +package com.tangem.features.hotwallet.addexistingwallet.entry import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.router.stack.replaceAll import com.arkivanov.decompose.router.stack.replaceCurrent import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped @@ -10,7 +11,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent @@ -43,14 +44,31 @@ internal class AddExistingWalletModel @Inject constructor( when (currentRoute) { is AddExistingWalletRoute.Import -> stackNavigation.pop() is AddExistingWalletRoute.BackupCompleted -> Unit + is AddExistingWalletRoute.SetAccessCode -> Unit is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop() - is AddExistingWalletRoute.SetAccessCode -> stackNavigation.pop() is AddExistingWalletRoute.PushNotifications -> Unit is AddExistingWalletRoute.SetupFinished -> Unit is AddExistingWalletRoute.Start -> Unit } } + fun onSkipAccessCode() { + navigateToPushNotificationsOrNext() + } + + private fun navigateToPushNotificationsOrNext() { + modelScope.launch { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { + // is yet blocked by [REDACTED_TASK_KEY] + // stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } else { + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } + } + } + inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { override fun onBackClick() { router.pop() @@ -69,7 +87,7 @@ internal class AddExistingWalletModel @Inject constructor( inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { override fun onContinueClick() { - stackNavigation.push(AddExistingWalletRoute.SetAccessCode) + stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode) } } @@ -81,16 +99,7 @@ internal class AddExistingWalletModel @Inject constructor( inner class ConfirmAccessCodeModelCallbacks : ConfirmAccessCodeComponent.ModelCallbacks { override fun onAccessCodeConfirmed() { - modelScope.launch { - val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (shouldRequestPush) { - // is yet blocked by [REDACTED_TASK_KEY] - // stackNavigation.replaceCurrent(AddExistingWalletRoute.PushNotifications) - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished) - } else { - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished) - } - } + navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt new file mode 100644 index 0000000000..14eabf9ebc --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletStepperStateManager.kt @@ -0,0 +1,79 @@ +package com.tangem.features.hotwallet.addexistingwallet.entry + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +internal class AddExistingWalletStepperStateManager { + + fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is AddExistingWalletRoute.Start -> null + + is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_IMPORT, + steps = STEPS_COUNT, + title = resourceReference(R.string.wallet_import_seed_navtitle), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + + is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = false, + showSkipButton = true, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = true, + showSkipButton = true, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_NOTIFICATIONS, + steps = STEPS_COUNT, + title = resourceReference(R.string.onboarding_title_notifications), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + + is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_DONE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 5 + + private const val STEP_IMPORT = 1 + private const val STEP_BACKUP = 2 + private const val STEP_ACCESS_CODE = 3 + private const val STEP_NOTIFICATIONS = 4 + private const val STEP_DONE = 5 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt index 1792942faf..53e611282b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.addexistingwallet.root +package com.tangem.features.hotwallet.addexistingwallet.entry import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable @@ -6,14 +6,16 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.hotwallet.AddExistingWalletComponent -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletChildFactory -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute -import com.tangem.features.hotwallet.addexistingwallet.root.ui.AddExistingWalletContent +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletChildFactory +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.entry.ui.AddExistingWalletContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,7 +23,9 @@ import dagger.assisted.AssistedInject internal class DefaultAddExistingWalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: Unit, + private val stepperStateManager: AddExistingWalletStepperStateManager, addExistingWalletChildFactory: AddExistingWalletChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, ) : AddExistingWalletComponent, AppComponentContext by appComponentContext { private val model: AddExistingWalletModel = getOrCreateModel(params) @@ -43,13 +47,42 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( }, ) + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM( + currentStep = 0, + steps = 0, + title = TextReference.EMPTY, + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ), + callback = object : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onChildBack() + } + + override fun onSkipClick() { + model.onSkipAccessCode() + } + }, + ), + ) + @Composable override fun Content(modifier: Modifier) { val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration BackHandler(onBack = ::onChildBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + AddExistingWalletContent( stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt index 0c6a950236..a8af84bae5 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/di/AddExistingWalletModule.kt @@ -1,11 +1,16 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.di +package com.tangem.features.hotwallet.addexistingwallet.entry.di import com.tangem.core.decompose.model.Model import com.tangem.features.hotwallet.AddExistingWalletComponent -import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel -import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletStepperStateManager +import com.tangem.features.hotwallet.addexistingwallet.entry.DefaultAddExistingWalletComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.HotWalletStepperModel import dagger.Binds import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey @@ -14,7 +19,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal interface AddExistingWalletModule { +internal interface AddExistingWalletModuleBinds { @Binds @Singleton @@ -26,4 +31,23 @@ internal interface AddExistingWalletModule { @IntoMap @ClassKey(AddExistingWalletModel::class) fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model + + @Binds + fun bindFactory(impl: DefaultHotWalletStepperComponent.Factory): HotWalletStepperComponent.Factory + + @Binds + @IntoMap + @ClassKey(HotWalletStepperModel::class) + fun bindHotWalletStepperModel(model: HotWalletStepperModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object AddExistingWalletModule { + + @Provides + @Singleton + fun provideAddExistingWalletStepperStateManager(): AddExistingWalletStepperStateManager { + return AddExistingWalletStepperStateManager() + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt index 8030308588..fb538425f2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/entity/AddExistingWalletUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.entity +package com.tangem.features.hotwallet.addexistingwallet.entry.entity internal data class AddExistingWalletUM( val onBackClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 38642f07bf..10bdd316a8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -1,10 +1,10 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.routing +package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent -import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel +import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt index f4f83e4193..f1102b80eb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.routing +package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.core.decompose.navigation.Route import kotlinx.serialization.Serializable diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt index bc43867900..f0fd3f4adf 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/ui/AddExistingWalletContent.kt @@ -1,6 +1,7 @@ -package com.tangem.features.hotwallet.addexistingwallet.root.ui +package com.tangem.features.hotwallet.addexistingwallet.entry.ui import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.systemBarsPadding @@ -12,19 +13,29 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent @Composable -internal fun AddExistingWalletContent(stackState: ChildStack) { - Children( - stack = stackState, - animation = stackAnimation(slide()), +internal fun AddExistingWalletContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, +) { + Column( modifier = Modifier .background(color = TangemTheme.colors.background.primary) .fillMaxSize() .imePadding() .systemBarsPadding(), ) { - it.instance.Content(Modifier.fillMaxSize()) + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt new file mode 100644 index 0000000000..c14de91c72 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.hotwallet.stepper.api + +import androidx.annotation.IntRange +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.TextReference +import kotlinx.coroutines.flow.StateFlow + +interface HotWalletStepperComponent : ComposableContentComponent { + + data class StepperUM( + @IntRange(from = 0) val currentStep: Int, + @IntRange(from = 0) val steps: Int, + val title: TextReference, + val showBackButton: Boolean, + val showSkipButton: Boolean, + val showFeedbackButton: Boolean, + ) + + interface ModelCallback { + fun onBackClick() + fun onSkipClick() + } + + class Params( + val initState: StepperUM, + val callback: ModelCallback, + ) + + val state: StateFlow + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt new file mode 100644 index 0000000000..211c9265f3 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/di/HotWalletStepperModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.hotwallet.stepper.di + +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface HotWalletStepperModule { + + @Binds + fun bindHotWalletStepperComponentFactory( + impl: DefaultHotWalletStepperComponent.Factory, + ): HotWalletStepperComponent.Factory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt new file mode 100644 index 0000000000..320920105b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/DefaultHotWalletStepperComponent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.hotwallet.stepper.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.ui.HotWalletStepper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultHotWalletStepperComponent @AssistedInject constructor( + @Assisted val context: AppComponentContext, + @Assisted val params: HotWalletStepperComponent.Params, +) : HotWalletStepperComponent, AppComponentContext by context { + + private val model: HotWalletStepperModel = getOrCreateModel(params) + + override val state = model.uiState + + fun updateState(newState: HotWalletStepperComponent.StepperUM) { + model.updateState(newState) + } + + @Composable + override fun Content(modifier: Modifier) { + val uiState by state.collectAsStateWithLifecycle() + + HotWalletStepper( + state = uiState, + modifier = modifier, + onBackClick = model::onBackClick, + onSkipClick = model::onSkipClick, + onFeedbackClick = model::onFeedbackClick, + ) + } + + @AssistedFactory + interface Factory : HotWalletStepperComponent.Factory { + override fun create( + context: AppComponentContext, + params: HotWalletStepperComponent.Params, + ): DefaultHotWalletStepperComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt new file mode 100644 index 0000000000..ed64c2fb93 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/HotWalletStepperModel.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.stepper.impl + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class HotWalletStepperModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(params.initState) + + fun updateState(newState: HotWalletStepperComponent.StepperUM) { + uiState.value = newState + } + + fun onBackClick() { + params.callback.onBackClick() + } + + fun onSkipClick() { + // TODO send analytics + params.callback.onSkipClick() + } + + fun onFeedbackClick() { + // TODO send analytics + // openFeedback() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt new file mode 100644 index 0000000000..f9af5a29b8 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt @@ -0,0 +1,106 @@ +package com.tangem.features.hotwallet.stepper.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun HotWalletStepper( + state: HotWalletStepperComponent.StepperUM, + onBackClick: () -> Unit, + onSkipClick: () -> Unit, + onFeedbackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction) + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemTopAppBar( + startButton = if (state.showBackButton) { + TopAppBarButtonUM.Back { onBackClick() } + } else { + null + }, + endButton = when { + state.showSkipButton -> TopAppBarButtonUM.Text( + text = resourceReference(R.string.common_skip), + onClicked = { onSkipClick() }, + ) + state.showFeedbackButton -> TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_chat_24, + onClicked = { onFeedbackClick() }, + ) + else -> null + }, + title = state.title, + containerColor = TangemTheme.colors.background.primary, + modifier = modifier, + titleAlignment = Alignment.CenterHorizontally, + ) + + TangemLinearProgressIndicator( + modifier = Modifier + .padding(horizontal = 16.dp) + .height(4.dp) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f), + strokeCap = StrokeCap.Round, + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun HotWalletStepper_Preview() { + TangemThemePreview { + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + HotWalletStepper( + modifier = Modifier.align(Alignment.TopCenter), + state = HotWalletStepperComponent.StepperUM( + currentStep = 2, + steps = 3, + title = resourceReference(R.string.common_done), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ), + onBackClick = {}, + onSkipClick = {}, + onFeedbackClick = {}, + ) + } + } +} \ No newline at end of file From 95c984d9e3edbf629c2f255f4725754ed6534d6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Jul 2025 13:59:01 +0500 Subject: [PATCH 014/165] Updated on 2026-08-14 --- .../hotwallet/accesscode/Constants.kt | 3 -- .../confirm/ConfirmAccessCodeComponent.kt | 52 ------------------- .../confirm/entity/ConfirmAccessCodeUM.kt | 12 ----- .../accesscode/di/AccessCodeModule.kt | 26 ---------- .../accesscode/set/SetAccessCodeModel.kt | 45 ---------------- .../accesscode/set/entity/SetAccessCodeUM.kt | 12 ----- .../accesscode/set/ui/SetAccessCodeContent.kt | 46 ---------------- .../HotAccessCodeRequestModel.kt | 5 +- .../entry/AddExistingWalletModel.kt | 10 ++-- .../routing/AddExistingWalletChildFactory.kt | 20 +++---- .../AccessCodeComponent.kt} | 29 ++++++++--- .../AccessCodeModel.kt} | 28 ++++++---- .../hotwallet/setaccesscode/Constants.kt | 3 ++ .../setaccesscode/di/AccessCodeModule.kt | 20 +++++++ .../setaccesscode/entity/AccessCodeUM.kt | 12 +++++ .../ui/AccessCodeEnter.kt | 2 +- .../ui/AccessCodeLayout.kt} | 25 +++++---- .../stepper/impl/ui/HotWalletStepper.kt | 6 +-- 18 files changed, 107 insertions(+), 249 deletions(-) delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{accesscode/set/SetAccessCodeComponent.kt => setaccesscode/AccessCodeComponent.kt} (62%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{accesscode/confirm/ConfirmAccessCodeModel.kt => setaccesscode/AccessCodeModel.kt} (53%) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{accesscode => setaccesscode}/ui/AccessCodeEnter.kt (98%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{accesscode/confirm/ui/ConfirmAccessCodeContent.kt => setaccesscode/ui/AccessCodeLayout.kt} (59%) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt deleted file mode 100644 index ca5ecaa367..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.features.hotwallet.accesscode - -const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt deleted file mode 100644 index 849eb2cb57..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeComponent.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.confirm - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.features.hotwallet.accesscode.confirm.ui.SetAccessCodeConfirmContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class ConfirmAccessCodeComponent @AssistedInject constructor( - @Assisted private val context: AppComponentContext, - @Assisted private val params: Params, -) : ComposableContentComponent, AppComponentContext by context { - - private val model: ConfirmAccessCodeModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - - DisableScreenshotsDisposableEffect() - - SetAccessCodeConfirmContent( - accessCode = state.accessCode, - onAccessCodeChange = state.onAccessCodeChange, - accessCodeLength = state.accessCodeLength, - onConfirm = state.onConfirm, - buttonEnabled = state.buttonEnabled, - modifier = modifier, - ) - } - - interface ModelCallbacks { - fun onAccessCodeConfirmed() - } - - data class Params( - val accessCodeToConfirm: String, - val callbacks: ModelCallbacks, - ) - - @AssistedFactory - interface Factory { - fun create(context: AppComponentContext, params: Params): ConfirmAccessCodeComponent - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt deleted file mode 100644 index f2df925578..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/entity/ConfirmAccessCodeUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.confirm.entity - -import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH - -internal data class ConfirmAccessCodeUM( - val accessCode: String, - val onAccessCodeChange: (String) -> Unit, - val buttonEnabled: Boolean, - val onConfirm: () -> Unit, -) { - val accessCodeLength: Int = ACCESS_CODE_LENGTH -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt deleted file mode 100644 index 8c79d221da..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.di - -import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeModel -import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(SingletonComponent::class) -internal interface AccessCodeModule { - - @Binds - @IntoMap - @ClassKey(SetAccessCodeModel::class) - fun bindSetAccessCodeModel(model: SetAccessCodeModel): Model - - @Binds - @IntoMap - @ClassKey(ConfirmAccessCodeModel::class) - fun bindConfirmAccessCodeModel(model: ConfirmAccessCodeModel): Model -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt deleted file mode 100644 index 4f57d50886..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.set - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.hotwallet.accesscode.set.entity.SetAccessCodeUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import javax.inject.Inject - -@Stable -@ModelScoped -internal class SetAccessCodeModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - internal val uiState: StateFlow - field = MutableStateFlow(getInitialState()) - - private fun getInitialState() = SetAccessCodeUM( - accessCode = "", - onAccessCodeChange = ::onAccessCodeChange, - buttonEnabled = false, - onContinue = ::onContinue, - ) - - private fun onAccessCodeChange(value: String) { - uiState.update { - it.copy( - accessCode = value, - buttonEnabled = value.length == uiState.value.accessCodeLength, - ) - } - } - - private fun onContinue() { - params.callbacks.onAccessCodeSet(uiState.value.accessCode) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt deleted file mode 100644 index 68fa89c2fe..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/entity/SetAccessCodeUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.set.entity - -import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH - -internal data class SetAccessCodeUM( - val accessCode: String, - val onAccessCodeChange: (String) -> Unit, - val buttonEnabled: Boolean, - val onContinue: () -> Unit, -) { - val accessCodeLength: Int = ACCESS_CODE_LENGTH -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt deleted file mode 100644 index 0fcf53c2df..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/ui/SetAccessCodeContent.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.features.hotwallet.accesscode.set.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.hotwallet.accesscode.ui.AccessCodeEnter -import com.tangem.core.res.R - -@Composable -internal fun SetAccessCodeContent( - accessCode: String, - onAccessCodeChange: (String) -> Unit, - accessCodeLength: Int, - onContinue: () -> Unit, - buttonEnabled: Boolean, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - ) { - AccessCodeEnter( - modifier = Modifier - .padding(top = 16.dp) - .weight(1f), - accessCode = accessCode, - onAccessCodeChange = onAccessCodeChange, - accessCodeLength = accessCodeLength, - reEnterAccessCodeState = false, - ) - - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .imePadding(), - text = stringResourceSafe(R.string.common_continue), - onClick = onContinue, - enabled = buttonEnabled, - ) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index c34ec66391..05e2ef611b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -82,8 +83,4 @@ internal class HotAccessCodeRequestModel @Inject constructor( it.copy(isShown = false) } } - - private companion object { - const val ACCESS_CODE_LENGTH = 6 - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index ad499a7aa4..1d192ffae5 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -14,8 +14,7 @@ import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWallet import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent -import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent -import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent +import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @@ -34,8 +33,7 @@ internal class AddExistingWalletModel @Inject constructor( val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks() val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks() - val setAccessCodeModelCallbacks = SetAccessCodeModelCallbacks() - val confirmAccessCodeModelCallbacks = ConfirmAccessCodeModelCallbacks() + val accessCodeModelCallbacks = AccessCodeModelCallbacks() val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks() val stackNavigation = StackNavigation() @@ -91,13 +89,11 @@ internal class AddExistingWalletModel @Inject constructor( } } - inner class SetAccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks { + inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { override fun onAccessCodeSet(accessCode: String) { stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(accessCode)) } - } - inner class ConfirmAccessCodeModelCallbacks : ConfirmAccessCodeComponent.ModelCallbacks { override fun onAccessCodeConfirmed() { navigateToPushNotificationsOrNext() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 10bdd316a8..90ffa797d0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -2,8 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.hotwallet.accesscode.set.SetAccessCodeComponent -import com.tangem.features.hotwallet.accesscode.confirm.ConfirmAccessCodeComponent +import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -14,8 +13,7 @@ import javax.inject.Inject internal class AddExistingWalletChildFactory @Inject constructor( private val pushNotificationsComponent: PushNotificationsComponent.Factory, - private val setAccessCodeSetComponentFactory: SetAccessCodeComponent.Factory, - private val confirmAccessCodeComponentFactory: ConfirmAccessCodeComponent.Factory, + private val accessCodeComponentFactory: AccessCodeComponent.Factory, ) { fun createChild( @@ -42,17 +40,19 @@ internal class AddExistingWalletChildFactory @Inject constructor( callbacks = model.manualBackupCompletedComponentModelCallbacks, ), ) - is AddExistingWalletRoute.SetAccessCode -> setAccessCodeSetComponentFactory.create( + is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create( context = childContext, - params = SetAccessCodeComponent.Params( - callbacks = model.setAccessCodeModelCallbacks, + params = AccessCodeComponent.Params( + isConfirmMode = false, + callbacks = model.accessCodeModelCallbacks, ), ) - is AddExistingWalletRoute.ConfirmAccessCode -> confirmAccessCodeComponentFactory.create( + is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( context = childContext, - params = ConfirmAccessCodeComponent.Params( + params = AccessCodeComponent.Params( + isConfirmMode = true, accessCodeToConfirm = route.accessCode, - callbacks = model.confirmAccessCodeModelCallbacks, + callbacks = model.accessCodeModelCallbacks, ), ) is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt similarity index 62% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt index c8a03346c0..4200b68d42 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/set/SetAccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.accesscode.set +package com.tangem.features.hotwallet.setaccesscode import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,17 +8,19 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.features.hotwallet.accesscode.set.ui.SetAccessCodeContent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.features.hotwallet.setaccesscode.ui.AccessCodeLayout +import com.tangem.core.res.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -internal class SetAccessCodeComponent @AssistedInject constructor( +internal class AccessCodeComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, @Assisted private val params: Params, ) : ComposableContentComponent, AppComponentContext by context { - private val model: SetAccessCodeModel = getOrCreateModel(params) + private val model: AccessCodeModel = getOrCreateModel(params) @Composable override fun Content(modifier: Modifier) { @@ -26,26 +28,37 @@ internal class SetAccessCodeComponent @AssistedInject constructor( DisableScreenshotsDisposableEffect() - SetAccessCodeContent( + AccessCodeLayout( + modifier = modifier, accessCode = state.accessCode, onAccessCodeChange = state.onAccessCodeChange, accessCodeLength = state.accessCodeLength, - onContinue = state.onContinue, + reEnterAccessCodeState = params.isConfirmMode, + buttonText = stringResourceSafe( + if (params.isConfirmMode) { + R.string.common_confirm + } else { + R.string.common_continue + }, + ), + onButtonClick = state.onButtonClick, buttonEnabled = state.buttonEnabled, - modifier = modifier, ) } interface ModelCallbacks { fun onAccessCodeSet(accessCode: String) + fun onAccessCodeConfirmed() } data class Params( + val isConfirmMode: Boolean, + val accessCodeToConfirm: String? = null, val callbacks: ModelCallbacks, ) @AssistedFactory interface Factory { - fun create(context: AppComponentContext, params: Params): SetAccessCodeComponent + fun create(context: AppComponentContext, params: Params): AccessCodeComponent } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt similarity index 53% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt index 25a77bdbdc..d978ba32f6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ConfirmAccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt @@ -1,10 +1,10 @@ -package com.tangem.features.hotwallet.accesscode.confirm +package com.tangem.features.hotwallet.setaccesscode import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.hotwallet.accesscode.confirm.entity.ConfirmAccessCodeUM +import com.tangem.features.hotwallet.setaccesscode.entity.AccessCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -13,33 +13,41 @@ import javax.inject.Inject @Stable @ModelScoped -internal class ConfirmAccessCodeModel @Inject constructor( +internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - private val params = paramsContainer.require() + private val params = paramsContainer.require() - internal val uiState: StateFlow + internal val uiState: StateFlow field = MutableStateFlow(getInitialState()) - private fun getInitialState() = ConfirmAccessCodeUM( + private fun getInitialState() = AccessCodeUM( accessCode = "", onAccessCodeChange = ::onAccessCodeChange, buttonEnabled = false, - onConfirm = ::onConfirm, + onButtonClick = ::onButtonClick, ) private fun onAccessCodeChange(value: String) { uiState.update { it.copy( accessCode = value, - buttonEnabled = value == params.accessCodeToConfirm, + buttonEnabled = if (params.isConfirmMode) { + value == params.accessCodeToConfirm + } else { + value.length == uiState.value.accessCodeLength + }, ) } } - private fun onConfirm() { - params.callbacks.onAccessCodeConfirmed() + private fun onButtonClick() { + if (params.isConfirmMode) { + params.callbacks.onAccessCodeConfirmed() + } else { + params.callbacks.onAccessCodeSet(uiState.value.accessCode) + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt new file mode 100644 index 0000000000..8494ba6e8b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt @@ -0,0 +1,3 @@ +package com.tangem.features.hotwallet.setaccesscode + +const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt new file mode 100644 index 0000000000..c6fb7f93cc --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.setaccesscode.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.setaccesscode.AccessCodeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccessCodeModule { + + @Binds + @IntoMap + @ClassKey(AccessCodeModel::class) + fun bindAccessCodeModel(model: AccessCodeModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt new file mode 100644 index 0000000000..836a816175 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.hotwallet.setaccesscode.entity + +import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH + +internal data class AccessCodeUM( + val accessCode: String, + val onAccessCodeChange: (String) -> Unit, + val buttonEnabled: Boolean, + val onButtonClick: () -> Unit, +) { + val accessCodeLength: Int = ACCESS_CODE_LENGTH +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt similarity index 98% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt index ba80fd0bb6..62e4388d9a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCodeEnter.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.accesscode.ui +package com.tangem.features.hotwallet.setaccesscode.ui import android.content.res.Configuration import androidx.compose.foundation.background diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt similarity index 59% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt index d85e2ea0d1..aee581c976 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/confirm/ui/ConfirmAccessCodeContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt @@ -1,20 +1,25 @@ -package com.tangem.features.hotwallet.accesscode.confirm.ui +package com.tangem.features.hotwallet.setaccesscode.ui -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.hotwallet.accesscode.ui.AccessCodeEnter -import com.tangem.core.res.R +@Suppress("LongParameterList") @Composable -internal fun SetAccessCodeConfirmContent( +internal fun AccessCodeLayout( accessCode: String, onAccessCodeChange: (String) -> Unit, accessCodeLength: Int, - onConfirm: () -> Unit, + reEnterAccessCodeState: Boolean, + buttonText: String, + onButtonClick: () -> Unit, buttonEnabled: Boolean, modifier: Modifier = Modifier, ) { @@ -30,7 +35,7 @@ internal fun SetAccessCodeConfirmContent( accessCode = accessCode, onAccessCodeChange = onAccessCodeChange, accessCodeLength = accessCodeLength, - reEnterAccessCodeState = true, + reEnterAccessCodeState = reEnterAccessCodeState, ) PrimaryButton( @@ -38,8 +43,8 @@ internal fun SetAccessCodeConfirmContent( .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .imePadding(), - text = stringResourceSafe(R.string.common_confirm), - onClick = onConfirm, + text = buttonText, + onClick = onButtonClick, enabled = buttonEnabled, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt index f9af5a29b8..ea029d37be 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/impl/ui/HotWalletStepper.kt @@ -43,18 +43,18 @@ internal fun HotWalletStepper( ) { TangemTopAppBar( startButton = if (state.showBackButton) { - TopAppBarButtonUM.Back { onBackClick() } + TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { state.showSkipButton -> TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), - onClicked = { onSkipClick() }, + onClicked = onSkipClick, ) state.showFeedbackButton -> TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, - onClicked = { onFeedbackClick() }, + onClicked = onFeedbackClick, ) else -> null }, From 79eab436df5c220b1ce32a6f354b7e1cda9ff742 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 15:11:40 +0300 Subject: [PATCH 015/165] Updated on 2026-08-14 --- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 14 +++++++++++--- .../kotlin/com/tangem/tests/OrganizeTokensTest.kt | 8 ++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index d65c64ed2b..1c05fa58dc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -348,10 +348,18 @@ class BuyTokenTest : BaseTestCase() { onBuyTokenDetailsScreen { providerTitle.performClick() } } step("Assert available provider name is displayed") { - onSelectProviderBottomSheet { availableProviderItem.assertIsDisplayed() } + onSelectProviderBottomSheet { + flakySafely(timeoutMs = 20_000) { + availableProviderItem.assertIsDisplayed() + } + } } step("Assert unavailable provider name is displayed") { - onSelectProviderBottomSheet { unavailableProviderItem.assertIsDisplayed() } + onSelectProviderBottomSheet { + flakySafely(timeoutMs = 20_000) { + unavailableProviderItem.assertIsDisplayed() + } + } } step("Click on 'Expand payment methods' button") { onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } @@ -393,7 +401,7 @@ class BuyTokenTest : BaseTestCase() { } } - @AllureId("2570") + @AllureId("3479") @DisplayName("Onramp: validate 'Select payment method' bottom sheet") @Test fun validatePaymentMethodScreenTest() { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index e64113e05e..6d89ee640d 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -79,12 +79,16 @@ class OrganizeTokensTest : BaseTestCase() { setupHooks().run { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" + val balance = "$184.85" step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } step("Check positions of tokens on 'Main Screen'") { onMainScreen { tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() @@ -154,12 +158,16 @@ class OrganizeTokensTest : BaseTestCase() { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" val polygonTitle = "Polygon" + val balance = "$184.85" step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Assert wallet balance = '$balance'") { + onMainScreen { walletBalance().assertTextContains(balance) } + } step("Check positions of tokens on 'Main Screen'") { onMainScreen { tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed() From 78720c5cab895049e3c7e46caaab1d93b2f47e31 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 11:18:48 +0400 Subject: [PATCH 016/165] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 1 + .../token/converter/YieldBalanceConverter.kt | 64 +++--- .../DefaultSingleYieldBalanceProducer.kt | 7 +- .../store/DefaultYieldsBalancesStore.kt | 16 +- .../tangem/data/staking/YieldBalanceExt.kt | 2 +- .../DefaultSingleYieldBalanceProducerTest.kt | 4 +- .../YieldsBalancesStoreUpdateMethodsTest.kt | 7 +- domain/staking/build.gradle.kts | 1 + domain/staking/models/build.gradle.kts | 1 + .../tangem/domain/staking/model/StakingID.kt | 4 +- .../staking/model/stakekit/YieldBalance.kt | 203 +++++------------- .../model/stakekit/YieldBalanceItem.kt | 130 +++++++++++ .../BaseCurrencyStatusOperations.kt | 2 +- .../CachedCurrenciesStatusesOperations.kt | 16 +- .../operations/CurrencyStatusOperations.kt | 3 +- .../utils/CurrencyStatusProxyCreator.kt | 10 +- features/staking/impl/build.gradle.kts | 1 + .../state/converters/BalanceItemConverter.kt | 6 +- .../converters/YieldBalancesConverter.kt | 2 +- gradle/dependencies.toml | 2 + 20 files changed, 267 insertions(+), 215 deletions(-) create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 7c44fe9963..0bd12e57fd 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { /** Coroutines */ implementation(deps.kotlin.coroutines) implementation(deps.kotlin.coroutines.rx2) + implementation(deps.kotlin.datetime) /** Logging */ implementation(deps.timber) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index de55373f39..5d86e9a0be 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -1,53 +1,61 @@ package com.tangem.datasource.local.token.converter +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.utils.converter.Converter +import kotlinx.datetime.Instant class YieldBalanceConverter( private val source: StatusSource, -) : Converter { +) : Converter { constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL) - override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { + override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? { + val stakingId = StakingID( + integrationId = value.integrationId ?: return null, + address = value.addresses.address, + ) + return if (value.balances.isEmpty()) { - YieldBalance.Empty( - integrationId = value.integrationId, - address = value.addresses.address, - source = source, - ) + YieldBalance.Empty(stakingId = stakingId, source = source) } else { YieldBalance.Data( - integrationId = value.integrationId, - address = value.addresses.address, + stakingId = stakingId, balance = YieldBalanceItem( - items = value.balances.map { item -> - BalanceItem( - groupId = item.groupId, - token = TokenConverter.convert(item.tokenDTO), - type = BalanceTypeConverter.convert(item.type), - amount = item.amount, - rawCurrencyId = item.tokenDTO.coinGeckoId, - // tron-specific. operates validatorAddresses instead of validatorAddress - validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), - date = item.date?.toDateTime(), - pendingActions = PendingActionConverter - .convertList(item.pendingActions) - .sortedBy { it.passthrough }, - pendingActionsConstraints = PendingActionConstraintsConverter - .convertList(item.pendingActionConstraints.orEmpty()), - isPending = false, - ) - } - .sortedWith(compareBy({ it.type }, { it.amount })), + items = value.balances + .map { item -> item.toBalanceItem() } + .sortedWith(comparator = compareBy({ it.type }, { it.amount })), integrationId = value.integrationId, ), source = source, ) } } + + private fun BalanceDTO.toBalanceItem(): BalanceItem { + val item = this + + return BalanceItem( + groupId = item.groupId, + token = TokenConverter.convert(item.tokenDTO), + type = BalanceTypeConverter.convert(item.type), + amount = item.amount, + rawCurrencyId = item.tokenDTO.coinGeckoId, + // tron-specific. operates validatorAddresses instead of validatorAddress + validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), + date = item.date?.toString()?.let { Instant.parse(it) }, + pendingActions = PendingActionConverter + .convertList(item.pendingActions) + .sortedBy { it.passthrough }, + pendingActionsConstraints = PendingActionConstraintsConverter + .convertList(item.pendingActionConstraints.orEmpty()), + isPending = false, + ) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index cac9b3c1a3..b84ffe3439 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -35,10 +35,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( ) : SingleYieldBalanceProducer { override val fallback: YieldBalance by lazy { - YieldBalance.Error( - integrationId = params.stakingId.integrationId, - address = params.stakingId.address, - ) + YieldBalance.Error(stakingId = params.stakingId) } override fun produce(): Flow { @@ -50,7 +47,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( .mapNotNull { balances -> val currentStakingId = params.stakingId - val currentBalances = balances.filter { it.getStakingId() == currentStakingId } + val currentBalances = balances.filter { it.stakingId == currentStakingId } if (currentBalances.size > 1) { analyticsExceptionHandler.sendException( diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt index 10186038e7..797c65a1cc 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt @@ -46,6 +46,8 @@ internal class DefaultYieldsBalancesStore( value = cachedStatuses.map { (stringWalletId, wrappers) -> val key = UserWalletId(stringWalletId) val value = YieldBalanceConverter(isCached = true).convertSet(input = wrappers) + .filterNotNull() + .toSet() key to value } @@ -61,7 +63,7 @@ internal class DefaultYieldsBalancesStore( override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { return runtimeStore.getSyncOrNull() ?.get(userWalletId) - ?.firstOrNull { stakingId == it.getStakingId() } + ?.firstOrNull { it.stakingId == stakingId } } override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { @@ -96,11 +98,13 @@ internal class DefaultYieldsBalancesStore( private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = values) + .filterNotNull() + .toSet() runtimeStore.update(default = emptyMap()) { saved -> saved.toMutableMap().apply { this[userWalletId] = saved[userWalletId] - ?.addOrReplace(newBalances) { old, new -> old.getStakingId() == new.getStakingId() } + ?.addOrReplace(newBalances) { old, new -> old.stakingId == new.stakingId } ?: newBalances } } @@ -135,7 +139,7 @@ internal class DefaultYieldsBalancesStore( val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId -> val balance = portfolioBalances - .firstOrNull { stakingId == it.getStakingId() } + .firstOrNull { it.stakingId == stakingId } ?: ifNotFound(stakingId) ?: return@mapNotNullTo null @@ -143,7 +147,7 @@ internal class DefaultYieldsBalancesStore( } val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> - old.getStakingId() == new.getStakingId() + old.stakingId == new.stakingId } put(key = userWalletId, value = updatedBalances) @@ -151,9 +155,7 @@ internal class DefaultYieldsBalancesStore( } } - private fun createErrorYieldBalance(id: StakingID): YieldBalance { - return YieldBalance.Error(integrationId = id.integrationId, address = id.address) - } + private fun createErrorYieldBalance(id: StakingID): YieldBalance = YieldBalance.Error(stakingId = id) private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? { val integrationId = integrationId diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt index c04560fbeb..6481744478 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/YieldBalanceExt.kt @@ -6,5 +6,5 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource.CACHE): YieldBalance { - return YieldBalanceConverter(source = source).convert(this) + return YieldBalanceConverter(source = source).convert(this)!! } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index b6f14b7e6b..022f10c3fc 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -84,7 +84,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val producerFlow = producer.produceWithFallback() val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() - val updatedBalance = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address) + val updatedBalance = YieldBalance.Error(stakingId = tonId) // Act (first emit) multiFlow.emit(value = setOf(balance)) @@ -162,7 +162,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val actual1 = getEmittedValues(flow = producerFlow) // Assert (first emit) - val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = "0x1") + val fallbackStatus = YieldBalance.Error(stakingId = tonId.copy(address = "0x1")) Truth.assertThat(actual1).hasSize(1) Truth.assertThat(actual1).containsExactly(fallbackStatus) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt index 9d80251ee9..1bece04b93 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/store/YieldsBalancesStoreUpdateMethodsTest.kt @@ -129,12 +129,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( - userWalletId to setOf( - YieldBalance.Error( - integrationId = stakingId.integrationId, - address = stakingId.address, - ), - ), + userWalletId to setOf(YieldBalance.Error(stakingId)), ) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 770bfe4838..7b65991ac1 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { api(projects.core.analytics) api(projects.core.utils) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts index 10abc551b9..5d2fa9274e 100644 --- a/domain/staking/models/build.gradle.kts +++ b/domain/staking/models/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.models) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt index 6313b9277c..59618e15b0 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt @@ -1,4 +1,6 @@ package com.tangem.domain.staking.model -// TODO: make part of YieldBalance in the future +import kotlinx.serialization.Serializable + +@Serializable data class StakingID(val integrationId: String, val address: String) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 44e0681f52..1dd03af2b9 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -2,166 +2,67 @@ package com.tangem.domain.staking.model.stakekit import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import org.joda.time.DateTime -import java.math.BigDecimal +import kotlinx.serialization.Serializable -sealed class YieldBalance { +/** + * Represents a yield balance in the staking system + */ +@Serializable +sealed interface YieldBalance { - abstract val integrationId: String? - abstract val address: String? - abstract val source: StatusSource + /** The unique identifier of the staking operation */ + val stakingId: StakingID + /** The source of the status information */ + val source: StatusSource + + /** + * Represents a yield balance with actual data + * + * @property stakingId the unique identifier of the staking operation + * @property source the source of the status information + * @property balance the balance details of the yield + */ + @Serializable + data class Data( + override val stakingId: StakingID, + override val source: StatusSource, + val balance: YieldBalanceItem, + ) : YieldBalance + + /** + * Represents an empty yield balance + * + * @property stakingId the unique identifier of the staking operation + * @property source the source of the status information + */ + @Serializable + data class Empty( + override val stakingId: StakingID, + override val source: StatusSource, + ) : YieldBalance + + /** + * Represents an error state for the yield balance + * + * @property stakingId the unique identifier of the staking operation + */ + @Serializable + data class Error(override val stakingId: StakingID) : YieldBalance { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Creates a copy of the current yield balance with a new status source + * + * @param source the new source of the status information + */ fun copySealed(source: StatusSource): YieldBalance { return when (this) { is Data -> copy(source = source) is Empty -> copy(source = source) is Error, - is Unsupported, -> this } } - - fun getStakingId(): StakingID? { - val integrationId = integrationId - val address = address - - if (integrationId == null || address == null) return null - - return StakingID(integrationId = integrationId, address = address) - } - - data class Data( - override val integrationId: String?, - override val address: String, - override val source: StatusSource, - val balance: YieldBalanceItem, - ) : YieldBalance() - - data class Empty( - override val integrationId: String?, - override val address: String, - override val source: StatusSource, - ) : YieldBalance() - - data object Unsupported : YieldBalance() { - override val integrationId: String? = null - override val address: String? = null - override val source: StatusSource = StatusSource.ACTUAL - } - - data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() { - override val source: StatusSource = StatusSource.ACTUAL - } -} - -data class YieldBalanceItem( - val items: List, - val integrationId: String?, -) - -data class BalanceItem( - val groupId: String, - val token: Token, - val type: BalanceType, - val amount: BigDecimal, - val rawCurrencyId: String?, - val validatorAddress: String?, - val date: DateTime?, - val pendingActions: List, - val pendingActionsConstraints: List, - val isPending: Boolean, -) - -data class PendingActionConstraints( - val type: StakingActionType, - val amountArg: PendingAction.PendingActionArgs.Amount?, -) - -data class PendingAction( - val type: StakingActionType, - val passthrough: String, - val args: PendingActionArgs?, -) { - data class PendingActionArgs( - val amount: Amount?, - val duration: Duration?, - val validatorAddress: Boolean?, - val validatorAddresses: Boolean?, - val tronResource: TronResource?, - val signatureVerification: Boolean?, - ) { - data class Amount( - val required: Boolean, - val minimum: BigDecimal?, - val maximum: BigDecimal?, - ) - - data class Duration( - val required: Boolean, - val minimum: Int?, - val maximum: Int?, - ) - - data class TronResource( - val required: Boolean, - val options: List, - ) - } -} - -/** - * IMPORTANT!!! - * Order is used to sort balances. - */ -@Suppress("MagicNumber") -enum class BalanceType(val order: Int) { - AVAILABLE(1), - STAKED(2), - PREPARING(3), - LOCKED(4), - UNSTAKING(5), - UNLOCKING(6), - UNSTAKED(7), - REWARDS(8), - UNKNOWN(9), - ; - - companion object { - fun BalanceType.isClickable() = when (this) { - STAKED, - UNSTAKED, - LOCKED, - -> true - AVAILABLE, - UNSTAKING, - PREPARING, - REWARDS, - UNLOCKING, - UNKNOWN, - -> false - } - } -} - -enum class RewardBlockType { - NoRewards, - Rewards, - RewardsRequirementsError, - RewardUnavailable, - ; - - /** - * Indicated whether action can be performed on available reward - * For example, pending action CLAIM_REWARDS could be called - */ - val isActionable: Boolean - get() = when (this) { - NoRewards, - RewardUnavailable, - -> false - RewardsRequirementsError, - Rewards, - -> true - } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt new file mode 100644 index 0000000000..b5bbd086b5 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt @@ -0,0 +1,130 @@ +package com.tangem.domain.staking.model.stakekit + +import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +@Serializable +data class YieldBalanceItem( + val items: List, + val integrationId: String, +) + +@Serializable +data class BalanceItem( + val groupId: String, + val token: Token, + val type: BalanceType, + val amount: SerializedBigDecimal, + val rawCurrencyId: String?, + val validatorAddress: String?, + val date: Instant?, + val pendingActions: List, + val pendingActionsConstraints: List, + val isPending: Boolean, +) + +@Serializable +data class PendingActionConstraints( + val type: StakingActionType, + val amountArg: PendingAction.PendingActionArgs.Amount?, +) + +@Serializable +data class PendingAction( + val type: StakingActionType, + val passthrough: String, + val args: PendingActionArgs?, +) { + + @Serializable + data class PendingActionArgs( + val amount: Amount?, + val duration: Duration?, + val validatorAddress: Boolean?, + val validatorAddresses: Boolean?, + val tronResource: TronResource?, + val signatureVerification: Boolean?, + ) { + + @Serializable + data class Amount( + val required: Boolean, + val minimum: SerializedBigDecimal?, + val maximum: SerializedBigDecimal?, + ) + + @Serializable + data class Duration( + val required: Boolean, + val minimum: Int?, + val maximum: Int?, + ) + + @Serializable + data class TronResource( + val required: Boolean, + val options: List, + ) + } +} + +/** + * IMPORTANT!!! + * Order is used to sort balances. + */ +@Serializable +@Suppress("MagicNumber") +enum class BalanceType(val order: Int) { + AVAILABLE(1), + STAKED(2), + PREPARING(3), + LOCKED(4), + UNSTAKING(5), + UNLOCKING(6), + UNSTAKED(7), + REWARDS(8), + UNKNOWN(9), + ; + + companion object { + + fun BalanceType.isClickable() = when (this) { + STAKED, + UNSTAKED, + LOCKED, + -> true + AVAILABLE, + UNSTAKING, + PREPARING, + REWARDS, + UNLOCKING, + UNKNOWN, + -> false + } + } +} + +@Serializable +enum class RewardBlockType { + NoRewards, + Rewards, + RewardsRequirementsError, + RewardUnavailable, + ; + + /** + * Indicated whether action can be performed on available reward + * For example, pending action CLAIM_REWARDS could be called + */ + val isActionable: Boolean + get() = when (this) { + NoRewards, + RewardUnavailable, + -> false + RewardsRequirementsError, + Rewards, + -> true + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 9aaf902fa7..70f695f4ef 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -420,7 +420,7 @@ abstract class BaseCurrencyStatusOperations( params = MultiYieldBalanceProducer.Params(userWalletId = userWalletId), ) .orEmpty() - .filter { it.getStakingId() in stakingIds } + .filter { it.stakingId in stakingIds } ensure(balances.isNotEmpty()) { Error.EmptyYieldBalances } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 0948480baa..797a76ca5f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -25,6 +25,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -277,12 +278,17 @@ class CachedCurrenciesStatusesOperations( ): YieldBalance? { if (yieldBalances.isNullOrEmpty()) return null - val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value ?: return null - + val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value val address = extractAddress(networkStatus) - return yieldBalances.firstOrNull { it.integrationId == supportedIntegration && it.address == address } - ?: YieldBalance.Error(integrationId = supportedIntegration, address = address) + return if (supportedIntegration != null && address != null) { + val stakingId = StakingID(integrationId = supportedIntegration, address = address) + + yieldBalances.firstOrNull { it.stakingId == stakingId } + ?: YieldBalance.Error(stakingId = stakingId) + } else { + null + } } private fun getCurrencies(userWalletId: UserWalletId): EitherFlow> { @@ -390,7 +396,7 @@ class CachedCurrenciesStatusesOperations( ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.getStakingId() == it } + loadedBalances.addOrReplace(balance) { balance.stakingId == it } } } .launchIn(scope = this) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 82c396b43b..ca1b7b0fcb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -80,7 +80,8 @@ internal class CurrencyStatusOperations( val hasCurrentNetworkTransactions = networkStatusValue.pendingTransactions.isNotEmpty() val currentTransactions = networkStatusValue.pendingTransactions.getOrElse(currency.id, ::emptySet) val yieldBalanceData = yieldBalance as? YieldBalance.Data - val isCurrentAddressStaking = yieldBalanceData?.address == networkStatusValue.address.defaultAddress.value + val isCurrentAddressStaking = + yieldBalanceData?.stakingId?.address == networkStatusValue.address.defaultAddress.value val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { it.token.coinGeckoId == currency.id.rawCurrencyId?.value } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index c1778e6af1..53b7db96ff 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -7,6 +7,7 @@ import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -74,9 +75,12 @@ class CurrencyStatusProxyCreator { val address = extractAddress(networkStatus) val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value - val yieldBalance = if (supportedIntegration != null) { - yieldBalances?.firstOrNull { it.integrationId == supportedIntegration && it.address == address } - ?: YieldBalance.Error(integrationId = supportedIntegration, address = address) + + val yieldBalance = if (supportedIntegration != null && address != null) { + val stakingId = StakingID(integrationId = supportedIntegration, address = address) + + yieldBalances?.firstOrNull { it.stakingId == stakingId } + ?: YieldBalance.Error(stakingId = stakingId) } else { null } diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 762201aacb..67246e25f1 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(deps.androidx.paging.runtime) /** Other dependencies */ + implementation(deps.kotlin.datetime) implementation(deps.kotlin.immutable.collections) implementation(deps.material) implementation(deps.arrow.core) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 6b747f4dbd..bdc0f43c77 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -22,7 +22,7 @@ import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toPersistentList -import org.joda.time.DateTime +import kotlinx.datetime.Instant import java.math.BigDecimal import java.util.Calendar @@ -122,7 +122,7 @@ internal class BalanceItemConverter( -> null } - private fun getUnbondingDate(date: DateTime?): TextReference? { + private fun getUnbondingDate(date: Instant?): TextReference? { val unbondingPeriod = yield.metadata.cooldownPeriod?.days ?: return null if (date == null) { return combinedReference( @@ -136,7 +136,7 @@ internal class BalanceItemConverter( nowCalendar.resetHours() val endDate = Calendar.getInstance() - endDate.timeInMillis = date.millis + endDate.timeInMillis = date.toEpochMilliseconds() endDate.resetHours() val days = ((endDate.timeInMillis - nowCalendar.timeInMillis) / DAY_IN_MILLIS).toInt() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index ee906adb33..f82c1ece1f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -50,7 +50,7 @@ internal class YieldBalancesConverter( ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } InnerYieldBalanceState.Data( - integrationId = yieldBalance?.integrationId, + integrationId = yieldBalance?.stakingId?.integrationId, reward = YieldReward( rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = fiatRewardsValue.format { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 03d9b1f740..1f665af06c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -73,6 +73,7 @@ viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" zxingQrCode = "3.5.1" kotlinSerialization = "1.8.0" +kotlinDatetime = "0.6.2" arrow = "1.2.4" # 2.0.1 breaks the build reownCore = "1.1.2" reownWeb3 = "1.1.2" @@ -258,6 +259,7 @@ viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydeleg xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" } kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" } +kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinDatetime" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" } arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reownCore = { module = "com.reown:android-core", version.ref = "reownCore" } From fb54de419b0410683984152f36cb8386e10c07e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 17:56:53 +0500 Subject: [PATCH 017/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../kotlin/com/tangem/tests/StoriesTest.kt | 4 +- .../main/java/com/tangem/tap/MainActivity.kt | 31 +- .../analytics/events/IntroductionProcess.kt | 18 -- .../tap/common/analytics/events/Shop.kt | 30 -- .../CardContextInterceptor.kt | 2 +- .../tap/common/compose/extensions/Dp.kt | 17 -- .../tap/common/compose/extensions/Painter.kt | 20 -- .../com/tangem/tap/common/extensions/Int.kt | 3 - .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../tangem/tap/common/ui/ScanFailsDialog.kt | 8 +- .../com/tangem/tap/di/IntentHandlingModule.kt | 34 +++ .../details/redux/DetailsMiddleware.kt | 2 +- .../ui/resetcard/model/ResetCardModel.kt | 2 +- .../tap/features/home/DefaultHomeComponent.kt | 90 ------ .../com/tangem/tap/features/home/HomeModel.kt | 166 ----------- .../tap/features/home/RegionProvider.kt | 16 -- .../home/TangemTangemErrorsHandler.kt | 44 --- .../tap/features/home/api/HomeComponent.kt | 9 - .../tap/features/home/di/HomeFeatureModule.kt | 25 -- .../home/errors/TangemSdkErrorHandler.kt | 8 - .../tap/features/home/redux/HomeAction.kt | 21 -- .../tap/features/home/redux/HomeMiddleware.kt | 147 ---------- .../tap/features/home/redux/HomeReducer.kt | 31 -- .../handlers/BackgroundScanIntentHandler.kt | 32 +-- .../WalletConnectLinkIntentHandler.kt | 2 +- .../welcome/component/WelcomeComponent.kt | 2 + .../features/welcome/model/WelcomeModel.kt | 8 +- .../welcome/redux/WelcomeMiddleware.kt | 15 +- .../tangem/tap/routing/utils/ChildFactory.kt | 7 +- .../com/tangem/common/routing/AppRoute.kt | 7 +- .../routing/entity/InitScreenLaunchMode.kt | 13 + .../routing/entity/SerializableIntent.kt | 1 + .../tangem/core/ui/utils}/AnimatedValue.kt | 2 +- .../com/tangem/core/ui/utils/DensityUtils.kt | 20 +- .../com/tangem/core/ui/utils}/ImageBitmap.kt | 2 +- .../res/drawable-hdpi/img_meet_tangem.webp | Bin .../img_revolutionary_wallet.webp | Bin .../img_tangem_for_everyone.webp | Bin .../res/drawable-mdpi/img_meet_tangem.webp | Bin .../img_revolutionary_wallet.webp | Bin .../img_tangem_for_everyone.webp | Bin .../res/drawable-xhdpi/img_meet_tangem.webp | Bin .../img_revolutionary_wallet.webp | Bin .../img_tangem_for_everyone.webp | Bin .../res/drawable-xxhdpi/img_meet_tangem.webp | Bin .../img_revolutionary_wallet.webp | Bin .../img_tangem_for_everyone.webp | Bin .../res/drawable-xxxhdpi/img_meet_tangem.webp | Bin .../img_revolutionary_wallet.webp | Bin .../img_tangem_for_everyone.webp | Bin .../ui}/src/main/res/drawable/currency0.webp | Bin .../ui}/src/main/res/drawable/currency1.webp | Bin .../ui}/src/main/res/drawable/currency2.webp | Bin .../ui}/src/main/res/drawable/currency3.webp | Bin .../ui}/src/main/res/drawable/currency4.webp | Bin .../ui}/src/main/res/drawable/dapps1.webp | Bin .../ui}/src/main/res/drawable/dapps2.webp | Bin .../ui}/src/main/res/drawable/dapps3.webp | Bin .../ui}/src/main/res/drawable/dapps4.webp | Bin .../ui}/src/main/res/drawable/dapps5.webp | Bin .../src/main/res/drawable/ic_tangem_logo.xml | 0 .../img_card_placeholder_wallet_2.webp | Bin .../java/com/tangem/utils/extensions/Int.kt | 6 - .../disclaimer/impl/model/DisclaimerModel.kt | 2 +- features/home/api/.gitignore | 1 + features/home/api/build.gradle.kts | 17 ++ .../tangem/features/home/api/HomeComponent.kt | 17 ++ features/home/impl/.gitignore | 1 + features/home/impl/build.gradle.kts | 69 +++++ .../home/impl/DefaultHomeComponent.kt | 40 +++ .../home/impl/analytics/AnalyticsParam.kt | 9 + .../impl/analytics/IntroductionProcess.kt | 14 + .../analytics/ParamCardCurrencyConverter.kt | 23 ++ .../features/home/impl/analytics/Shop.kt | 11 + .../home/impl/di/HomeFeatureModule.kt | 33 +++ .../features/home/impl/model/HomeModel.kt | 268 ++++++++++++++++++ .../com/tangem/features/home/impl/ui/Home.kt | 35 +++ .../home/impl/ui}/compose/StoriesAnimation.kt | 6 +- .../home/impl/ui}/compose/StoriesScreen.kt | 25 +- .../home/impl/ui}/compose/StoriesScreenV2.kt | 23 +- .../home/impl/ui}/compose/content/Content.kt | 8 +- .../compose/content/CurrenciesWeb3Content.kt | 18 +- .../compose/content/FirstStoriesContent.kt | 6 +- .../compose/content/FloatingCardsContent.kt | 10 +- .../impl/ui}/compose/views/HomeButtons.kt | 4 +- .../impl/ui}/compose/views/HomeButtonsV2.kt | 4 +- .../compose/views/SearchCurrenciesButton.kt | 4 +- .../impl/ui}/compose/views/StoriesButton.kt | 2 +- .../ui}/compose/views/StoriesProgressBar.kt | 2 +- .../features/home/impl/ui/state/HomeUM.kt | 19 +- .../entry/impl/model/OnboardingEntryModel.kt | 2 +- .../model/WalletSettingsModel.kt | 2 +- .../model/intents/WalletCardClickIntents.kt | 2 +- .../router/DefaultWalletRouter.kt | 2 +- .../features/welcome/WelcomeComponent.kt | 2 + settings.gradle.kts | 3 + 98 files changed, 750 insertions(+), 787 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Int.kt create mode 100644 app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/HomeModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt rename {app/src/main/java/com/tangem/tap/common/compose/extensions => core/ui/src/main/java/com/tangem/core/ui/utils}/AnimatedValue.kt (96%) rename {app/src/main/java/com/tangem/tap/common/compose/extensions => core/ui/src/main/java/com/tangem/core/ui/utils}/ImageBitmap.kt (92%) rename {app => core/ui}/src/main/res/drawable-hdpi/img_meet_tangem.webp (100%) rename {app => core/ui}/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp (100%) rename {app => core/ui}/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp (100%) rename {app => core/ui}/src/main/res/drawable-mdpi/img_meet_tangem.webp (100%) rename {app => core/ui}/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp (100%) rename {app => core/ui}/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp (100%) rename {app => core/ui}/src/main/res/drawable-xhdpi/img_meet_tangem.webp (100%) rename {app => core/ui}/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp (100%) rename {app => core/ui}/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxhdpi/img_meet_tangem.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp (100%) rename {app => core/ui}/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp (100%) rename {app => core/ui}/src/main/res/drawable/currency0.webp (100%) rename {app => core/ui}/src/main/res/drawable/currency1.webp (100%) rename {app => core/ui}/src/main/res/drawable/currency2.webp (100%) rename {app => core/ui}/src/main/res/drawable/currency3.webp (100%) rename {app => core/ui}/src/main/res/drawable/currency4.webp (100%) rename {app => core/ui}/src/main/res/drawable/dapps1.webp (100%) rename {app => core/ui}/src/main/res/drawable/dapps2.webp (100%) rename {app => core/ui}/src/main/res/drawable/dapps3.webp (100%) rename {app => core/ui}/src/main/res/drawable/dapps4.webp (100%) rename {app => core/ui}/src/main/res/drawable/dapps5.webp (100%) rename {app => core/ui}/src/main/res/drawable/ic_tangem_logo.xml (100%) rename {app => core/ui}/src/main/res/drawable/img_card_placeholder_wallet_2.webp (100%) delete mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/Int.kt create mode 100644 features/home/api/.gitignore create mode 100644 features/home/api/build.gradle.kts create mode 100644 features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt create mode 100644 features/home/impl/.gitignore create mode 100644 features/home/impl/build.gradle.kts create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt create mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/StoriesAnimation.kt (96%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/StoriesScreen.kt (94%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/StoriesScreenV2.kt (94%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/content/Content.kt (96%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/content/CurrenciesWeb3Content.kt (93%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/content/FirstStoriesContent.kt (94%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/content/FloatingCardsContent.kt (92%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/views/HomeButtons.kt (97%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/views/HomeButtonsV2.kt (98%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/views/SearchCurrenciesButton.kt (94%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/views/StoriesButton.kt (97%) rename {app/src/main/java/com/tangem/tap/features/home => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui}/compose/views/StoriesProgressBar.kt (98%) rename app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt => features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt (58%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 040b46b637..19dd55b0e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -232,6 +232,8 @@ dependencies { implementation(projects.features.welcome.impl) implementation(projects.features.createWalletSelection.api) implementation(projects.features.createWalletSelection.impl) + implementation(projects.features.home.api) + implementation(projects.features.home.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 303ce3f6a0..8fe8ee6203 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -3,8 +3,8 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion -import com.tangem.screens.onDisclaimerScreen -import com.tangem.screens.onStoriesScreen +import com.tangem.screens.DisclaimerTestScreen +import com.tangem.screens.StoriesTestScreen import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.intent.KIntent diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 776dbb7b61..7be16eab0a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -176,13 +176,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var testerMenuLauncher: TesterMenuLauncher + @Inject + internal lateinit var intentProcessor: IntentProcessor + + @Inject + internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler + + @Inject + internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler + + @Inject + internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow - // TODO: fixme: inject through DI - private val intentProcessor: IntentProcessor = IntentProcessor() - private val dialogManager = DialogManager() private val onActivityResultCallbacks = mutableListOf() @@ -344,12 +353,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun initIntentHandlers() { - val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } - intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler)) - intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) + intentProcessor.addHandler(onPushClickedIntentHandler) if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - intentProcessor.addHandler(WalletConnectLinkIntentHandler()) + intentProcessor.addHandler(walletConnectLinkIntentHandler) } } @@ -435,9 +442,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { + val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { - replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) + replaceAll( + AppRoute.Welcome( + launchMode = launchMode, + intent = intentWhichStartedActivity?.let(::SerializableIntent), + ), + ) } intentProcessor.handleIntent( intent = intentWhichStartedActivity, @@ -452,7 +465,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { val route = when { shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false) shouldShowInitialPush -> AppRoute.PushNotification - else -> AppRoute.Home + else -> AppRoute.Home(launchMode = launchMode) } store.dispatchNavigationAction { replaceAll(route) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt deleted file mode 100644 index b5ce0042ae..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent - -/** -[REDACTED_AUTHOR] - */ -sealed class IntroductionProcess( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Introduction Process", event, params) { - - class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") - class ButtonTokensList : IntroductionProcess("Button - Tokens List") - class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") - class ButtonScanCard : IntroductionProcess("Button - Scan Card") - class ButtonRequestSupport : IntroductionProcess("Button - Request Support") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt deleted file mode 100644 index 4076de8685..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.tap.common.extensions.filterNotNull - -/** -[REDACTED_AUTHOR] - */ -sealed class Shop( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Shop", event, params) { - - class ScreenOpened : Shop("Shop Screen Opened") - - class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop( - event = "Purchased", - params = mapOf( - "SKU" to sku, - "Count" to count, - "Amount" to amount, - "Coupon Code" to couponCode, - ).filterNotNull(), - ) - - class Redirected(partnerName: String?) : Shop( - event = "Redirected", - params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 654dbe07ca..8f14501047 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -6,9 +6,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.features.home.impl.analytics.IntroductionProcess import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt deleted file mode 100644 index 04240d7b55..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpSize - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun Dp.toPx(): Float { - val currentDp = this - return with(LocalDensity.current) { currentDp.toPx() } -} - -fun DpSize.halfHeight(): Dp = this.height / 2 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt deleted file mode 100644 index dfabb2cddf..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.dp -import com.tangem.sdk.extensions.pxToDp - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun Painter.dpSize(): DpSize = DpSize( - intrinsicSize.width.pxToDp().dp, - intrinsicSize.height.pxToDp().dp, -) - -@Composable -private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Int.kt b/app/src/main/java/com/tangem/tap/common/extensions/Int.kt deleted file mode 100644 index 555dc252bd..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Int.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.tap.common.extensions - -fun Int.isEven() = this and 1 == 0 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 111bad630c..8bd03da756 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -3,7 +3,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer -import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState { return AppState( globalState = globalReducer(action, state), - homeState = HomeReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 3df1fae79b..427a103a98 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState -import com.tangem.tap.features.home.redux.HomeMiddleware -import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware @@ -20,7 +18,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), - val homeState: HomeState = HomeState(), val detailsState: DetailsState = DetailsState(), val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), @@ -32,7 +29,6 @@ data class AppState( return listOf( logMiddleware, GlobalMiddleware.handler, - HomeMiddleware.handler, DetailsMiddleware().detailsMiddleware, WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index 6096730a8e..61b24a7d63 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -4,6 +4,7 @@ import android.content.Context import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.compose.ui.text.intl.Locale import androidx.core.view.isVisible import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam @@ -14,8 +15,6 @@ import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.inject -import com.tangem.tap.features.home.LocaleRegionProvider -import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -29,6 +28,7 @@ internal object ScanFailsDialog { private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" + private const val RUSSIA_LOCALE = "ru" fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { @@ -62,8 +62,8 @@ internal object ScanFailsDialog { source = sourceAnalytics, ), ) - val locale = LocaleRegionProvider().getRegion() - val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK + val locale = Locale.current.region + val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK store.dispatchOpenUrl(link) } customView.findViewById(R.id.request_support_button)?.setOnClickListener { diff --git a/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt new file mode 100644 index 0000000000..cd7becb2e0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.di + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.tap.features.intentHandler.IntentProcessor +import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler +import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler +import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object IntentHandlingModule { + + @Provides + @Singleton + fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler() + + @Provides + @Singleton + fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler() + + @Provides + @Singleton + fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler = + OnPushClickedIntentHandler(analyticsEventHandler) + + @Provides + @Singleton + fun provideIntentProcessor(): IntentProcessor = IntentProcessor() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index af4c05b261..3aece96fa9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -233,7 +233,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } return CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index 34e073bf69..2d0af3ca0b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -268,7 +268,7 @@ internal class ResetCardModel @Inject constructor( if (isLocked && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { popTo() } } else { - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt deleted file mode 100644 index b89b6f5255..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.SystemBarsIconsDisposable -import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect -import com.tangem.core.ui.utils.findActivity -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.home.api.HomeComponent -import com.tangem.tap.features.home.compose.StoriesScreen -import com.tangem.tap.features.home.compose.StoriesScreenV2 -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.store -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import org.rekotlin.StoreSubscriber - -@Suppress("UnusedPrivateMember") -internal class DefaultHomeComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber { - - private val model: HomeModel = getOrCreateModel() - - private var homeState: MutableState = mutableStateOf(store.state.homeState) - - init { - lifecycle.subscribe( - onCreate = { - store.dispatch(HomeAction.OnCreate) - }, - onStart = { - store.subscribe(subscriber = this) { state -> - state - .skipRepeats { oldState, newState -> oldState.homeState == newState.homeState } - .select(AppState::homeState) - } - }, - onStop = { - store.unsubscribe(this) - }, - ) - } - - @Composable - override fun Content(modifier: Modifier) { - val activity = LocalContext.current.findActivity() - BackHandler(onBack = activity::finish) - SystemBarsIconsDisposable(darkIcons = false) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - StoriesScreenV2( - homeState = homeState, - onCreateNewWalletButtonClick = model::onCreateNewWalletScreen, - onAddExistingWalletButtonClick = model::onAddExistingWalletScreen, - onScanButtonClick = model::onScanClick, - ) - } else { - StoriesScreen( - homeState = homeState, - onScanButtonClick = model::onScanClick, - onShopButtonClick = model::onShopClick, - onSearchTokensClick = model::onSearchClick, - ) - } - - ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) - } - - override fun newState(state: HomeState) { - homeState.value = state - } - - @AssistedFactory - interface Factory : HomeComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt deleted file mode 100644 index 6110c7ff0c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.compose.runtime.Stable -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRoute.ManageTokens.Source -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.tokens.TokensAction -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.tap.common.analytics.events.IntroductionProcess -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import java.util.Locale -import javax.inject.Inject - -@Suppress("LongParameterList") -@Stable -@ModelScoped -internal class HomeModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, - private val cardSdkConfigRepository: CardSdkConfigRepository, - private val settingsRepository: SettingsRepository, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val router: Router, - getUserCountryUseCase: GetUserCountryUseCase, -) : Model() { - - private val tangemErrorHandler = TangemTangemErrorsHandler(store) - - init { - getUserCountryUseCase.invoke() - .distinctUntilChanged() - .filterNotNull() - .onEach { - val userCountry = it.getOrNull() ?: UserCountry.Other(Locale.getDefault().country) - store.dispatchOnMain(HomeAction.UserCountryLoaded(userCountry)) - } - .flowOn(dispatchers.io) - .launchIn(modelScope) - } - - fun onCreateNewWalletScreen() { - router.push(AppRoute.CreateWalletSelection) - } - - fun onAddExistingWalletScreen() { - router.push(AppRoute.AddExistingWallet) - } - - fun onScanClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) - scanCard() - } - - fun onShopClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards()) - analyticsEventHandler.send(Shop.ScreenOpened()) - - Firebase.analytics.appInstanceId - .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } - .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } - } - - fun onSearchClick() { - analyticsEventHandler.send(IntroductionProcess.ButtonTokensList()) - - store.dispatch(TokensAction.SetArgs.ReadAccess) - store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) } - } - - private fun scanCard() { - modelScope.launch { - cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes() - - scanCardProcessor.scan( - analyticsSource = AnalyticsParam.ScreensSources.Intro, - onProgressStateChange = { showProgress -> - if (showProgress) { - store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) - } else { - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - } - }, - onFailure = { - tangemErrorHandler.onErrorReceived(error = it) - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - }, - onSuccess = ::proceedWithScanResponse, - ) - } - } - - private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { - val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() - - if (userWallet == null) { - Timber.e("User wallet not created") - return - } - - saveWalletUseCase(userWallet).fold( - ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") }, - ifRight = { - sendSignedInCardAnalyticsEvent(scanResponse) - coroutineScope { store.onUserWalletSelected(userWallet = userWallet) } - }, - ) - - store.dispatchWithMain(HomeAction.ScanInProgress(scanInProgress = false)) - delay(HIDE_PROGRESS_DELAY) - - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - } - - private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { - val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) - - if (currency != null) { - Analytics.send( - event = Basic.SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = Basic.SignedIn.SignInType.Card, - walletsCount = "1", - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt b/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt deleted file mode 100644 index fb4ae25b2b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/RegionProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.features.home - -import androidx.compose.ui.text.intl.Locale - -/** -[REDACTED_AUTHOR] - */ -interface RegionProvider { - fun getRegion(): String? -} - -class LocaleRegionProvider : RegionProvider { - override fun getRegion(): String = Locale.current.region -} - -const val RUSSIA_COUNTRY_CODE = "ru" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt b/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt deleted file mode 100644 index 40e51a8de8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/TangemTangemErrorsHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.home - -import com.tangem.blockchain.common.BlockchainError -import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError -import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.home.errors.TangemSdkErrorHandler -import org.rekotlin.Store -import timber.log.Timber - -class TangemTangemErrorsHandler(val store: Store) : TangemSdkErrorHandler { - - override fun onErrorReceived(error: TangemError) { - when (error) { - is TangemSdkError -> { - handleCardSdkError(error) - } - is BlockchainError -> { - handleBlockchainSdkError(error) - } - else -> { - Timber.e("Error happened", error) - } - } - } - - private fun handleCardSdkError(error: TangemSdkError) { - when (error) { - is TangemSdkError.NfcFeatureIsUnavailable -> { - store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable)) - } - else -> { - Timber.e(error, "Unable to scan card") - } - } - } - - private fun handleBlockchainSdkError(error: TangemError) { - Timber.e("Sdk error happened", error) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt deleted file mode 100644 index 0c63e10a1c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/api/HomeComponent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.home.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent - -interface HomeComponent : ComposableContentComponent { - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt b/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt deleted file mode 100644 index 9f92cc5583..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/di/HomeFeatureModule.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.home.di - -import com.tangem.core.decompose.model.Model -import com.tangem.tap.features.home.DefaultHomeComponent -import com.tangem.tap.features.home.HomeModel -import com.tangem.tap.features.home.api.HomeComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(SingletonComponent::class) -internal interface HomeFeatureModule { - - @Binds - fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory - - @Binds - @IntoMap - @ClassKey(HomeModel::class) - fun bindModel(model: HomeModel): Model -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt b/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt deleted file mode 100644 index 7946b7ec5e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/errors/TangemSdkErrorHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.features.home.errors - -import com.tangem.common.core.TangemError - -interface TangemSdkErrorHandler { - - fun onErrorReceived(error: TangemError) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt deleted file mode 100644 index 9a449e938e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.home.redux - -import com.tangem.domain.settings.usercountry.models.UserCountry -import kotlinx.coroutines.CoroutineScope -import org.rekotlin.Action - -sealed class HomeAction : Action { - - data object OnCreate : HomeAction() - - /** - * Action for scanning card - * - * @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed - */ - data class ReadCard(val scope: CoroutineScope) : HomeAction() - - data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() - - data class UserCountryLoaded(val userCountry: UserCountry) : HomeAction() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt deleted file mode 100644 index e66b7f8c84..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.tap.features.home.redux - -import android.content.res.Resources -import com.tangem.common.doOnFailure -import com.tangem.common.doOnResult -import com.tangem.common.doOnSuccess -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.tap.common.analytics.events.IntroductionProcess -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.eraseContext -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import timber.log.Timber -import java.util.Locale - -internal const val HIDE_PROGRESS_DELAY = 400L - -object HomeMiddleware { - val handler = homeMiddleware - - private val SYSTEM_LANGUAGE = - runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" } - private val APP_LANGUAGE = Locale.getDefault().language - private val UTM_MARKS = "utm_source=tangem-app" + - "&utm_medium=app" + - "&utm_campaign=prospect-$SYSTEM_LANGUAGE" + - "&utm_content=devicelang-$APP_LANGUAGE" - - val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?$UTM_MARKS" -} - -private val homeMiddleware: Middleware = { _, _ -> - { next -> - { action -> - handleHomeAction(action) - next(action) - } - } -} - -private fun handleHomeAction(action: Action) { - when (action) { - is HomeAction.OnCreate -> { - Analytics.eraseContext() - Analytics.send(IntroductionProcess.ScreenOpened()) - - store.dispatch(GlobalAction.RestoreAppCurrency) - } - is HomeAction.ReadCard -> { - action.scope.launch { - readCard() - } - } - } -} - -private suspend fun readCard() { - val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = shouldSaveAccessCodes, - ) - - store.inject(DaggerGraphState::scanCardProcessor).scan( - analyticsSource = AnalyticsParam.ScreensSources.Intro, - onProgressStateChange = { showProgress -> - if (showProgress) { - store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) - } else { - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - } - }, - onFailure = { - Timber.e(it, "Unable to scan card") - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) - }, - onSuccess = { scanResponse -> - proceedWithScanResponse(scanResponse) - }, - ) -} - -private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse) - - val userWallet = userWalletBuilder.build().guard { - Timber.e("User wallet not created") - return@launch - } - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.save(userWallet) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - .doOnSuccess { - sendSignedInCardAnalyticsEvent(scanResponse) - store.onUserWalletSelected(userWallet = userWallet) - } - .doOnResult { - navigateTo(AppRoute.Wallet) - } -} - -private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { - val currency = ParamCardCurrencyConverter().convert( - value = scanResponse.cardTypesResolver, - ) - - if (currency != null) { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - Analytics.send( - event = Basic.SignedIn( - currency = currency, - batch = scanResponse.card.batchId, - signInType = Basic.SignedIn.SignInType.Card, - walletsCount = userWalletsListManager.walletsCount.toString(), - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } -} - -private suspend fun navigateTo(route: AppRoute) { - store.dispatchNavigationAction { push(route) } - delay(HIDE_PROGRESS_DELAY) - store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt deleted file mode 100644 index 678de45d76..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.features.home.redux - -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.tap.common.redux.AppState -import kotlinx.collections.immutable.toImmutableList -import org.rekotlin.Action - -object HomeReducer { - fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, appState: AppState): HomeState { - if (action !is HomeAction) return appState.homeState - - return when (action) { - is HomeAction.ScanInProgress -> { - appState.homeState.copy(scanInProgress = action.scanInProgress) - } - is HomeAction.UserCountryLoaded -> { - val stories = if (action.userCountry.needApplyFCARestrictions()) { - getRestrictedStories() - } else { - Stories.entries - } - appState.homeState.copy( - stories = stories.toImmutableList(), - ) - } - else -> appState.homeState - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 90ef042a6b..766392b9dd 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,21 +4,12 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.features.home.redux.HomeAction -import com.tangem.tap.features.intentHandler.IntentHandler -import com.tangem.tap.features.intentHandler.AffectsNavigation -import com.tangem.tap.features.welcome.redux.WelcomeAction -import com.tangem.tap.store -import kotlinx.coroutines.CoroutineScope +import com.tangem.common.routing.entity.InitScreenLaunchMode /** [REDACTED_AUTHOR] */ -class BackgroundScanIntentHandler( - private val hasSavedUserWalletsProvider: () -> Boolean, - private val scope: CoroutineScope, -) : IntentHandler, AffectsNavigation { +class BackgroundScanIntentHandler { private val nfcActions = arrayOf( NfcAdapter.ACTION_NDEF_DISCOVERED, @@ -26,8 +17,15 @@ class BackgroundScanIntentHandler( NfcAdapter.ACTION_TAG_DISCOVERED, ) - override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { - if (isFromForeground) return true + fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode { + return if (shouldOpenScanCard(intent)) { + InitScreenLaunchMode.WithCardScan + } else { + InitScreenLaunchMode.Standard + } + } + + private fun shouldOpenScanCard(intent: Intent?): Boolean { if (intent == null || intent.action !in nfcActions) return false val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -36,15 +34,9 @@ class BackgroundScanIntentHandler( @Suppress("DEPRECATION") intent.getParcelableExtra(NfcAdapter.EXTRA_TAG) } - if (tag == null) return false intent.action = null - if (hasSavedUserWalletsProvider.invoke()) { - store.dispatchOnMain(WelcomeAction.ProceedWithCard) - } else { - store.dispatchOnMain(HomeAction.ReadCard(scope = scope)) - } - return true + return tag != null } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 6f138c18e5..e715290a1e 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -4,8 +4,8 @@ import android.content.Intent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.intentHandler.AffectsNavigation +import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.store import timber.log.Timber import java.net.URLDecoder diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt index 07a92373e1..215560a1ad 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.component +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent @@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { data class Params( + val launchMode: InitScreenLaunchMode, val intent: SerializableIntent?, ) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt index 8b1dc10be1..5965f011f3 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.welcome.model import com.tangem.common.core.TangemError +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -44,10 +45,9 @@ internal class WelcomeModel @Inject constructor( subscribeToStoreChanges() initGlobalState() - val welcomeAction = if (params.intent != null) { - WelcomeAction.ProceedWithIntent(params.intent.toIntent()) - } else { - WelcomeAction.ProceedWithBiometrics() + val welcomeAction = when (params.launchMode) { + is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard + is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics(params.intent?.toIntent()) } store.dispatch(welcomeAction) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index ebbc8b2fc6..15c1ace4f6 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -20,10 +20,8 @@ import com.tangem.tap.* import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.proxy.redux.DaggerGraphState -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -44,7 +42,7 @@ internal class WelcomeMiddleware { private fun handleAction(action: WelcomeAction, state: WelcomeState) { mainScope.launch { when (action) { - is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this) + is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent) is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( afterUnlockIntent = action.afterUnlockIntent ?: state.intent, ) @@ -55,7 +53,7 @@ internal class WelcomeMiddleware { } } - private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) { + private suspend fun proceedWithIntent(initialIntent: Intent) { Timber.d( """ Proceeding with intent @@ -63,15 +61,12 @@ internal class WelcomeMiddleware { """.trimIndent(), ) - val handler = BackgroundScanIntentHandler( - scope = scope, - hasSavedUserWalletsProvider = { true }, - ) - val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false) val hasUncompletedBackup = backupService.hasIncompletedBackup - if (!isBackgroundScanHandled && !hasUncompletedBackup) { + if (!hasUncompletedBackup) { store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent)) + } else { + store.dispatchWithMain(WelcomeAction.ProceedWithCard) } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 894658edf5..9ecfe8e5c6 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -40,7 +40,7 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -import com.tangem.tap.features.home.api.HomeComponent +import com.tangem.features.home.api.HomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped @@ -131,6 +131,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = WelcomeComponent.Params( + launchMode = route.launchMode, intent = route.intent, ), componentFactory = welcomeComponentFactory, @@ -288,7 +289,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Home -> { createComponentChild( context = context, - params = Unit, + params = HomeComponent.Params(route.launchMode), componentFactory = homeComponentFactory, ) } @@ -380,7 +381,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.PushNotification -> { createComponentChild( context = context, - params = PushNotificationsComponent.Params.Route(AppRoute.Home), + params = PushNotificationsComponent.Params.Route(AppRoute.Home()), componentFactory = pushNotificationsComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 77a7b1f113..d1afb593f0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -3,6 +3,7 @@ package com.tangem.common.routing import android.os.Bundle import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency @@ -22,10 +23,14 @@ sealed class AppRoute(val path: String) : Route { data object Initial : AppRoute(path = "/initial") @Serializable - data object Home : AppRoute(path = "/home") + data class Home( + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + ) : AppRoute(path = "/home") @Serializable data class Welcome( + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + // we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware val intent: SerializableIntent? = null, ) : AppRoute(path = "/welcome"), RouteBundleParams { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt new file mode 100644 index 0000000000..3e1e008994 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/InitScreenLaunchMode.kt @@ -0,0 +1,13 @@ +package com.tangem.common.routing.entity + +import kotlinx.serialization.Serializable + +@Serializable +sealed class InitScreenLaunchMode { + + @Serializable + data object Standard : InitScreenLaunchMode() + + @Serializable + data object WithCardScan : InitScreenLaunchMode() +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt index 38ef6f2096..cb4f2b871a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt @@ -14,6 +14,7 @@ data class SerializableIntent( val packageValue: String?, val component: String?, val flags: Int, + // CAUTION: works wrong with SerializableBundle constructor(bundle: Bundle), need to be removed val extras: SerializableBundle?, ) { diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt rename to core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt index b21726c8e7..20ba1ea49a 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/AnimatedValue.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.compose.extensions +package com.tangem.core.ui.utils import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt index 105d2416d3..0a3bb10329 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt @@ -1,9 +1,15 @@ package com.tangem.core.ui.utils +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt @Stable @Composable @@ -15,4 +21,16 @@ fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.curr fun Dp.toPx(density: Float): Float = this.value * density -fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) \ No newline at end of file +fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) + +fun Context.dpToPx(dp: Float): Float = dp * resources.displayMetrics.density +fun Context.pxToDp(px: Float): Float = (px / resources.displayMetrics.density).roundToInt().toFloat() + +@Composable +fun Painter.dpSize(): DpSize = DpSize( + intrinsicSize.width.pxToDp().dp, + intrinsicSize.height.pxToDp().dp, +) + +@Composable +private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt rename to core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt index 99911d9e6f..4f4cff9559 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBitmap.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.compose.extensions +package com.tangem.core.ui.utils import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources diff --git a/app/src/main/res/drawable-hdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-hdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-hdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-mdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-mdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-mdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xxhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp diff --git a/app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp rename to core/ui/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable/currency0.webp b/core/ui/src/main/res/drawable/currency0.webp similarity index 100% rename from app/src/main/res/drawable/currency0.webp rename to core/ui/src/main/res/drawable/currency0.webp diff --git a/app/src/main/res/drawable/currency1.webp b/core/ui/src/main/res/drawable/currency1.webp similarity index 100% rename from app/src/main/res/drawable/currency1.webp rename to core/ui/src/main/res/drawable/currency1.webp diff --git a/app/src/main/res/drawable/currency2.webp b/core/ui/src/main/res/drawable/currency2.webp similarity index 100% rename from app/src/main/res/drawable/currency2.webp rename to core/ui/src/main/res/drawable/currency2.webp diff --git a/app/src/main/res/drawable/currency3.webp b/core/ui/src/main/res/drawable/currency3.webp similarity index 100% rename from app/src/main/res/drawable/currency3.webp rename to core/ui/src/main/res/drawable/currency3.webp diff --git a/app/src/main/res/drawable/currency4.webp b/core/ui/src/main/res/drawable/currency4.webp similarity index 100% rename from app/src/main/res/drawable/currency4.webp rename to core/ui/src/main/res/drawable/currency4.webp diff --git a/app/src/main/res/drawable/dapps1.webp b/core/ui/src/main/res/drawable/dapps1.webp similarity index 100% rename from app/src/main/res/drawable/dapps1.webp rename to core/ui/src/main/res/drawable/dapps1.webp diff --git a/app/src/main/res/drawable/dapps2.webp b/core/ui/src/main/res/drawable/dapps2.webp similarity index 100% rename from app/src/main/res/drawable/dapps2.webp rename to core/ui/src/main/res/drawable/dapps2.webp diff --git a/app/src/main/res/drawable/dapps3.webp b/core/ui/src/main/res/drawable/dapps3.webp similarity index 100% rename from app/src/main/res/drawable/dapps3.webp rename to core/ui/src/main/res/drawable/dapps3.webp diff --git a/app/src/main/res/drawable/dapps4.webp b/core/ui/src/main/res/drawable/dapps4.webp similarity index 100% rename from app/src/main/res/drawable/dapps4.webp rename to core/ui/src/main/res/drawable/dapps4.webp diff --git a/app/src/main/res/drawable/dapps5.webp b/core/ui/src/main/res/drawable/dapps5.webp similarity index 100% rename from app/src/main/res/drawable/dapps5.webp rename to core/ui/src/main/res/drawable/dapps5.webp diff --git a/app/src/main/res/drawable/ic_tangem_logo.xml b/core/ui/src/main/res/drawable/ic_tangem_logo.xml similarity index 100% rename from app/src/main/res/drawable/ic_tangem_logo.xml rename to core/ui/src/main/res/drawable/ic_tangem_logo.xml diff --git a/app/src/main/res/drawable/img_card_placeholder_wallet_2.webp b/core/ui/src/main/res/drawable/img_card_placeholder_wallet_2.webp similarity index 100% rename from app/src/main/res/drawable/img_card_placeholder_wallet_2.webp rename to core/ui/src/main/res/drawable/img_card_placeholder_wallet_2.webp diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt deleted file mode 100644 index 357b26203c..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.utils.extensions - -/** -[REDACTED_AUTHOR] - */ -fun Int.isEven(): Boolean = this % 2 == 0 \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index f96cb66625..1ea5597638 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -51,7 +51,7 @@ internal class DisclaimerModel @Inject constructor( } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } } diff --git a/features/home/api/.gitignore b/features/home/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/home/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/home/api/build.gradle.kts b/features/home/api/build.gradle.kts new file mode 100644 index 0000000000..7f07d748d7 --- /dev/null +++ b/features/home/api/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.home.api" +} + +dependencies { + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Common */ + implementation(projects.common.routing) +} \ No newline at end of file diff --git a/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt b/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt new file mode 100644 index 0000000000..05a94c2090 --- /dev/null +++ b/features/home/api/src/main/kotlin/com/tangem/features/home/api/HomeComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.home.api + +import com.tangem.common.routing.entity.InitScreenLaunchMode +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface HomeComponent : ComposableContentComponent { + + data class Params( + val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, + ) + + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Params): HomeComponent + } +} \ No newline at end of file diff --git a/features/home/impl/.gitignore b/features/home/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/home/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts new file mode 100644 index 0000000000..4a4f17d3bf --- /dev/null +++ b/features/home/impl/build.gradle.kts @@ -0,0 +1,69 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.home.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.home.api) + implementation(projects.features.hotWallet.api) + + /** Core modules */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.navigation) + + /** Common */ + implementation(projects.common.routing) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.core) + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.legacy) + implementation(projects.domain.feedback) + implementation(projects.domain.feedback.models) + + /** AndroidX libraries */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + + /** Firebase */ + implementation(deps.firebase.analytics) + + /** Tangem libraries */ + implementation(tangemDeps.card.android) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) + + /** Other libraries */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt new file mode 100644 index 0000000000..7c210b9af7 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.home.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.model.HomeModel +import com.tangem.features.home.impl.ui.Home +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultHomeComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: HomeComponent.Params, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, +) : HomeComponent, AppComponentContext by appComponentContext { + + private val model: HomeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + Home( + state = state, + modifier = modifier, + isV2StoriesEnabled = hotWalletFeatureToggles.isHotWalletEnabled, + ) + } + + @AssistedFactory + interface Factory : HomeComponent.Factory { + override fun create(context: AppComponentContext, params: HomeComponent.Params): DefaultHomeComponent + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt new file mode 100644 index 0000000000..def2ff2645 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt @@ -0,0 +1,9 @@ +package com.tangem.features.home.impl.analytics + +internal sealed class AnalyticsParam { + + sealed class CurrencyType(val value: String) { + class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) + class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt new file mode 100644 index 0000000000..6115389bfd --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt @@ -0,0 +1,14 @@ +package com.tangem.features.home.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class IntroductionProcess( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Introduction Process", event, params) { + + object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened") + object ButtonTokensList : IntroductionProcess("Button - Tokens List") + object ButtonBuyCards : IntroductionProcess("Button - Buy Cards") + object ButtonScanCard : IntroductionProcess("Button - Scan Card") +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt new file mode 100644 index 0000000000..16c104323b --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.features.home.impl.analytics + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.card.CardTypesResolver +import com.tangem.utils.converter.Converter +import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam + +internal class ParamCardCurrencyConverter : Converter { + + override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { + if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency + + val type = when { + value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) + value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin) + value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) + value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!) + else -> null + } ?: return null + + return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt new file mode 100644 index 0000000000..632cc66ef2 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt @@ -0,0 +1,11 @@ +package com.tangem.features.home.impl.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class Shop( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Shop", event, params) { + + object ScreenOpened : Shop("Shop Screen Opened") +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt new file mode 100644 index 0000000000..cb6516bc9b --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/di/HomeFeatureModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.home.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.DefaultHomeComponent +import com.tangem.features.home.impl.model.HomeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultHomeComponent.Factory): HomeComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(HomeModel::class) + fun provideModel(model: HomeModel): Model +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt new file mode 100644 index 0000000000..61614e3a62 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -0,0 +1,268 @@ +package com.tangem.features.home.impl.model + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRoute.ManageTokens.Source +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.entity.InitScreenLaunchMode +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.features.home.api.HomeComponent +import com.tangem.features.home.impl.analytics.IntroductionProcess +import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter +import com.tangem.features.home.impl.analytics.Shop +import com.tangem.features.home.impl.ui.state.HomeUM +import com.tangem.features.home.impl.ui.state.Stories +import com.tangem.features.home.impl.ui.state.getRestrictedStories +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject + +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") +@ModelScoped +internal class HomeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val scanCardProcessor: ScanCardProcessor, + private val saveWalletUseCase: SaveWalletUseCase, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val router: Router, + private val selectWalletUseCase: SelectWalletUseCase, + private val appRouter: AppRouter, + private val getUserCountryUseCase: GetUserCountryUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, +) : Model() { + + val params = paramsContainer.require() + + private val _uiState = MutableStateFlow( + HomeUM( + scanInProgress = false, + stories = getRestrictedStories().toImmutableList(), + onScanClick = ::onScanClick, + onShopClick = ::onShopClick, + onSearchTokensClick = ::onSearchTokensClick, + onCreateNewWalletClick = ::onCreateNewWalletClick, + onAddExistingWalletClick = ::onAddExistingWalletClick, + ), + ) + + val uiState = _uiState.asStateFlow() + + init { + analyticsEventHandler.send(IntroductionProcess.ScreenOpened) + observeUserCountryChanges() + + when (params.launchMode) { + InitScreenLaunchMode.Standard -> Unit + InitScreenLaunchMode.WithCardScan -> scanCard() + } + } + + private fun observeUserCountryChanges() { + getUserCountryUseCase.invoke() + .distinctUntilChanged() + .filterNotNull() + .onEach { result -> + val userCountry = result.getOrNull() ?: UserCountry.Other(Locale.getDefault().country) + updateStoriesForCountry(userCountry) + } + .flowOn(dispatchers.io) + .launchIn(modelScope) + } + + private fun updateStoriesForCountry(userCountry: UserCountry) { + val stories = if (userCountry.needApplyFCARestrictions()) { + getRestrictedStories() + } else { + Stories.entries + } + + _uiState.update { + it.copy(stories = stories.toImmutableList()) + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + + Firebase.analytics.appInstanceId + .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } + .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } + } + + private fun onSearchTokensClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonTokensList) + router.push(AppRoute.ManageTokens(Source.STORIES)) + } + + private fun onCreateNewWalletClick() { + router.push(AppRoute.CreateWalletSelection) + } + + private fun onAddExistingWalletClick() { + router.push(AppRoute.AddExistingWallet) + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + Timber.e(it.toString(), "Unable to save user wallet") + setLoading(false) + }, + ifRight = { + sendSignedInCardAnalyticsEvent(scanResponse) + + // Select the wallet using new mechanism + selectWalletUseCase(userWallet.walletId).fold( + ifLeft = { + Timber.e("Unable to select user wallet: $it") + setLoading(false) + }, + ifRight = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = "1", + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + _uiState.update { it.copy(scanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> { + handleNfcFeatureUnavailable() + } + is TangemSdkError -> { + Timber.e(error, "Scan error occurred") + } + else -> { + Timber.e(error, "Error happened") + } + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } + + companion object { + const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + } +} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt new file mode 100644 index 0000000000..24025e1627 --- /dev/null +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt @@ -0,0 +1,35 @@ +package com.tangem.features.home.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.SystemBarsIconsDisposable +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect +import com.tangem.features.home.impl.ui.compose.StoriesScreen +import com.tangem.features.home.impl.ui.compose.StoriesScreenV2 +import com.tangem.features.home.impl.ui.state.HomeUM + +@Composable +internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier = Modifier) { + SystemBarsIconsDisposable(darkIcons = false) + + if (isV2StoriesEnabled) { + StoriesScreenV2( + modifier = modifier, + state = state, + onCreateNewWalletButtonClick = state.onCreateNewWalletClick, + onAddExistingWalletButtonClick = state.onAddExistingWalletClick, + onScanButtonClick = state.onScanClick, + ) + } else { + StoriesScreen( + modifier = modifier, + state = state, + onScanButtonClick = state.onScanClick, + onShopButtonClick = state.onShopClick, + onSearchTokensClick = state.onSearchTokensClick, + ) + } + + ChangeRootBackgroundColorEffect(TangemColorPalette.Black) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt index 5265c91db4..3bf071d96a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesAnimation.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.animation.core.* import androidx.compose.foundation.Image @@ -19,8 +19,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.tangem.tap.common.compose.extensions.AnimatedValue -import com.tangem.tap.common.compose.extensions.toAnimatable +import com.tangem.core.ui.utils.AnimatedValue +import com.tangem.core.ui.utils.toAnimatable private const val SCALE_SWITCH_BARRIER = 1.15f diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt index 3ccd4ba7fe..704485cb28 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -23,24 +23,23 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.tap.features.home.compose.content.* -import com.tangem.tap.features.home.compose.views.HomeButtons -import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton -import com.tangem.tap.features.home.compose.views.StoriesProgressBar -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.home.redux.Stories -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.content.* +import com.tangem.features.home.impl.ui.compose.views.HomeButtons +import com.tangem.features.home.impl.ui.compose.views.SearchCurrenciesButton +import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar +import com.tangem.features.home.impl.ui.state.Stories +import com.tangem.core.ui.R +import com.tangem.features.home.impl.ui.state.HomeUM import kotlin.math.max @Composable internal fun StoriesScreen( - homeState: MutableState, + state: HomeUM, onScanButtonClick: () -> Unit, onShopButtonClick: () -> Unit, onSearchTokensClick: () -> Unit, + modifier: Modifier = Modifier, ) { - val state = homeState.value - var currentStory by remember { mutableStateOf(state.firstStory) } val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) @@ -59,14 +58,14 @@ internal fun StoriesScreen( // todo refactor [REDACTED_TASK_KEY] StoriesScreenContent( - modifier = Modifier + modifier = modifier .fillMaxSize() .testTag(StoriesScreenTestTags.SCREEN_CONTAINER), config = StoriesScreenContentConfig( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, currentStory = currentStory, - isScanInProgress = homeState.value.scanInProgress, + isScanInProgress = state.scanInProgress, onGoToPreviousStory = goToPreviousStory, onGoToNextStory = goToNextStory, onSearchTokensClick = onSearchTokensClick, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt index 5ba5f44315..0554472199 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreenV2.kt @@ -1,6 +1,6 @@ @file:Suppress("MagicNumber") -package com.tangem.tap.features.home.compose +package com.tangem.features.home.impl.ui.compose import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -20,23 +20,22 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.tap.features.home.compose.content.* -import com.tangem.tap.features.home.compose.views.HomeButtonsV2 -import com.tangem.tap.features.home.compose.views.StoriesProgressBar -import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.home.redux.Stories -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.content.* +import com.tangem.features.home.impl.ui.compose.views.HomeButtonsV2 +import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar +import com.tangem.features.home.impl.ui.state.Stories import kotlin.math.max +import com.tangem.core.ui.R +import com.tangem.features.home.impl.ui.state.HomeUM @Composable internal fun StoriesScreenV2( - homeState: MutableState, + state: HomeUM, onCreateNewWalletButtonClick: () -> Unit, onAddExistingWalletButtonClick: () -> Unit, onScanButtonClick: () -> Unit, + modifier: Modifier = Modifier, ) { - val state = homeState.value - var currentStory by remember { mutableStateOf(state.firstStory) } val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) @@ -55,14 +54,14 @@ internal fun StoriesScreenV2( // todo refactor [REDACTED_TASK_KEY] StoriesScreenContentV2( - modifier = Modifier + modifier = modifier .fillMaxSize() .testTag(StoriesScreenTestTags.SCREEN_CONTAINER), config = StoriesScreenContentV2Config( storiesSize = state.stories.lastIndex, currentStoryIndex = currentStoryIndex, currentStory = currentStory, - isScanInProgress = homeState.value.scanInProgress, + isScanInProgress = state.scanInProgress, onGoToPreviousStory = goToPreviousStory, onGoToNextStory = goToNextStory, onCreateNewWalletButtonClick = onCreateNewWalletButtonClick, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt similarity index 96% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt index 1635f234ad..33bf6db728 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/Content.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -21,9 +21,9 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation -import com.tangem.tap.features.home.compose.StoriesTextAnimation -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.StoriesBottomImageAnimation +import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation +import com.tangem.core.ui.R @Composable fun StoriesRevolutionaryWallet() { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt similarity index 93% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt index ec1cb88957..66f639bc51 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/CurrenciesWeb3Content.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -17,12 +17,10 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.extensions.dpSize -import com.tangem.tap.common.compose.extensions.halfHeight -import com.tangem.tap.common.compose.extensions.toPx -import com.tangem.tap.common.extensions.isEven -import com.tangem.tap.features.home.compose.HorizontalSlidingImage -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.HorizontalSlidingImage +import com.tangem.core.ui.R +import com.tangem.core.ui.utils.dpSize +import com.tangem.core.ui.utils.toPx @Composable fun StoriesCurrenciesContent(paused: Boolean, duration: Int) { @@ -138,4 +136,8 @@ private val BottomGradient: Brush = Brush.verticalGradient( TangemColorPalette.Black.copy(alpha = 0.95f), TangemColorPalette.Black, ), -) \ No newline at end of file +) + +fun DpSize.halfHeight(): Dp = this.height / 2 + +fun Int.isEven() = this and 1 == 0 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt index 42fdcaf8fa..f7f6debd9c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FirstStoriesContent.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing @@ -25,8 +25,8 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.home.compose.StoriesTextAnimation -import com.tangem.wallet.R +import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation +import com.tangem.core.ui.R @Suppress("LongMethod", "ComplexMethod", "MagicNumber") @Composable diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt index 6a010d28ed..e846781ec0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/content/FloatingCardsContent.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.content +package com.tangem.features.home.impl.ui.compose.content import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* @@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.graphicsLayer -import com.tangem.tap.common.compose.extensions.AnimatedValue -import com.tangem.tap.common.compose.extensions.asImageBitmap -import com.tangem.tap.common.compose.extensions.toAnimatable -import com.tangem.wallet.R +import com.tangem.core.ui.R +import com.tangem.core.ui.utils.AnimatedValue +import com.tangem.core.ui.utils.asImageBitmap +import com.tangem.core.ui.utils.toAnimatable /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt index eab6ba4ced..5b0615495b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtons.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun HomeButtons( diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt index dc2192b603..c68825891d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/HomeButtonsV2.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun HomeButtonsV2( diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt index 4d70d5f2f6..0987e2193b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/SearchCurrenciesButton.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -12,7 +12,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.wallet.R +import com.tangem.core.ui.R @Composable internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt index 66fc2bd6f1..ddcc652d3f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesButton.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesButton.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import androidx.compose.material3.ButtonColors import androidx.compose.runtime.Composable diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt index ef3514bce4..cc6ea2107c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/views/StoriesProgressBar.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.home.compose.views +package com.tangem.features.home.impl.ui.compose.views import android.provider.Settings import androidx.compose.animation.core.Animatable diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt rename to features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt index b1d45779ba..d727df8990 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/state/HomeUM.kt @@ -1,15 +1,16 @@ -package com.tangem.tap.features.home.redux +package com.tangem.features.home.impl.ui.state import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import org.rekotlin.StateType - -// todo refactor [REDACTED_TASK_KEY] -data class HomeState( - val scanInProgress: Boolean = false, - val stories: ImmutableList = getRestrictedStories().toImmutableList(), -) : StateType { +data class HomeUM( + val scanInProgress: Boolean, + val stories: ImmutableList, + val onScanClick: () -> Unit, + val onShopClick: () -> Unit, + val onSearchTokensClick: () -> Unit, + val onCreateNewWalletClick: () -> Unit, + val onAddExistingWalletClick: () -> Unit, +) { val firstStory: Stories get() = stories[0] fun stepOf(story: Stories): Int = stories.indexOf(story) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index ac205fd637..24dc8aaa95 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -206,7 +206,7 @@ internal class OnboardingEntryModel @Inject constructor( router.replaceAll(AppRoute.Wallet) } } else { - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 520d031d69..dcdfa6c203 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -197,7 +197,7 @@ internal class WalletSettingsModel @Inject constructor( if (hasUserWallets) { router.pop() } else { - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Home()) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt index 2ecee965b3..466f15890e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt @@ -102,7 +102,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( } else { tokenListStore.clear() stateHolder.clear() - appRouter.replaceAll(AppRoute.Home) + appRouter.replaceAll(AppRoute.Home()) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 2567421144..846fe77ccb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -75,7 +75,7 @@ internal class DefaultWalletRouter @Inject constructor( } override fun openStoriesScreen() { - router.push(AppRoute.Home) + router.push(AppRoute.Home()) } override fun isWalletLastScreen(): Boolean { diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt index 50b914548b..6d9044f216 100644 --- a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.welcome +import com.tangem.common.routing.entity.InitScreenLaunchMode import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent @@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { data class Params( + val launchMode: InitScreenLaunchMode, val intent: SerializableIntent?, ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 0550f19562..9eb66f4052 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -183,6 +183,9 @@ include(":libs:tangem-sdk-api") include(":features:onboarding-v2:api") include(":features:onboarding-v2:impl") +include(":features:home:api") +include(":features:home:impl") + include(":features:referral:api") include(":features:referral:data") include(":features:referral:domain") From 177da9798efe833fe57151f2233fa7c9b375ced0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 13:45:45 +0400 Subject: [PATCH 018/165] Updated on 2026-08-14 --- .../MockYieldBalanceWrapperDTOFactory.kt | 2 +- .../test/data/staking/MockYieldDTOFactory.kt | 2 +- .../ui/tokens/TokenItemStateConverter.kt | 2 +- .../models/request/ActionRequestBody.kt | 2 +- .../token/converter/BalanceTypeConverter.kt | 2 +- .../PendingActionConstraintsConverter.kt | 4 ++-- .../token/converter/PendingActionConverter.kt | 2 +- .../converter/StakingActionTypeConverter.kt | 2 +- .../converter/StakingNetworkTypeConverter.kt | 2 +- .../token/converter/YieldBalanceConverter.kt | 10 ++++---- ...kenConverter.kt => YieldTokenConverter.kt} | 10 ++++---- .../data/staking/DefaultStakingRepository.kt | 10 ++++---- .../data/staking/converters/YieldConverter.kt | 12 +++++----- .../transaction/GasEstimateConverter.kt | 4 ++-- .../multi/DefaultMultiYieldBalanceFetcher.kt | 2 +- .../multi/DefaultMultiYieldBalanceProducer.kt | 2 +- .../DefaultSingleYieldBalanceProducer.kt | 2 +- .../store/DefaultYieldsBalancesStore.kt | 4 ++-- .../data/staking/store/YieldsBalancesStore.kt | 4 ++-- .../YieldBalanceRequestBodyAddressFactory.kt | 2 +- .../utils/YieldBalanceRequestBodyFactory.kt | 2 +- .../tangem/data/staking/YieldBalanceExt.kt | 2 +- .../DefaultMultiYieldBalanceFetcherTest.kt | 2 +- .../DefaultMultiYieldBalanceProducerTest.kt | 4 ++-- .../DefaultSingleYieldBalanceProducerTest.kt | 4 ++-- .../store/YieldsBalancesStoreGetMethodTest.kt | 2 +- .../YieldsBalancesStoreInitializationTest.kt | 2 +- .../YieldsBalancesStoreUpdateMethodsTest.kt | 4 ++-- .../DefaultCurrencyChecksRepository.kt | 2 +- domain/models/build.gradle.kts | 2 ++ .../domain/models/staking}/NetworkType.kt | 5 +++- .../domain/models/staking}/StakingID.kt | 2 +- .../domain/models/staking}/YieldBalance.kt | 3 +-- .../models/staking}/YieldBalanceItem.kt | 6 ++--- .../domain/models/staking/YieldToken.kt | 15 ++++++++++++ .../staking}/action/StakingActionType.kt | 5 +++- .../domain/staking/model/stakekit/Yield.kt | 23 +++++-------------- .../model/stakekit/action/StakingAction.kt | 1 + .../stakekit/transaction/ActionParams.kt | 6 ++--- .../transaction/StakingGasEstimate.kt | 4 ++-- .../transaction/StakingTransaction.kt | 2 +- .../domain/staking/FetchActionsUseCase.kt | 2 +- .../GetActionRequirementAmountUseCase.kt | 2 +- .../InvalidatePendingTransactionsUseCase.kt | 16 ++++++++----- .../tangem/domain/staking/StakingIdFactory.kt | 2 +- .../analytics/StakingAnalyticsEvent.kt | 2 +- .../staking/multi/MultiYieldBalanceFetcher.kt | 2 +- .../multi/MultiYieldBalanceProducer.kt | 2 +- .../multi/MultiYieldBalanceSupplier.kt | 2 +- .../staking/repositories/StakingRepository.kt | 2 +- .../single/SingleYieldBalanceFetcher.kt | 2 +- .../single/SingleYieldBalanceProducer.kt | 4 ++-- .../single/SingleYieldBalanceSupplier.kt | 2 +- .../domain/staking/utils/YieldBalanceExt.kt | 4 ++-- .../domain/staking/StakingIdFactoryTest.kt | 2 +- .../tokens/model/CryptoCurrencyStatus.kt | 2 +- .../BaseCurrencyStatusOperations.kt | 4 ++-- .../CachedCurrenciesStatusesOperations.kt | 4 ++-- .../operations/CurrencyStatusOperations.kt | 2 +- .../TokenListFiatBalanceOperations.kt | 2 +- .../operations/TokenListSortingOperations.kt | 2 +- .../utils/CurrencyStatusProxyCreator.kt | 4 ++-- .../tokens/wallet/WalletBalanceFetcherTest.kt | 2 +- .../analytics/utils/StakingAnalyticSender.kt | 2 +- .../impl/presentation/model/StakingModel.kt | 7 +++++- .../state/InnerYieldBalanceState.kt | 4 ++++ .../impl/presentation/state/StakingUiState.kt | 2 +- ...StakingActionSelectionBottomSheetConfig.kt | 2 +- .../state/converters/BalanceItemConverter.kt | 10 ++++---- .../RewardsValidatorStateConverter.kt | 6 ++--- .../converters/YieldBalancesConverter.kt | 6 ++++- .../helpers/StakingFeeTransactionLoader.kt | 2 +- .../state/helpers/StakingTransactionSender.kt | 4 ++-- .../previewdata/InitialStakingStatePreview.kt | 4 ++-- .../SetConfirmationStateInitTransformer.kt | 4 ++-- .../SetInitialDataStateTransformer.kt | 2 +- ...howActionSelectorBottomSheetTransformer.kt | 2 +- .../StakingInfoNotificationsFactory.kt | 6 ++--- .../ValidatorSelectChangeTransformer.kt | 2 +- .../state/utils/StakingPendingActionUtils.kt | 6 ++--- .../ui/StakingInitialInfoContent.kt | 4 ++-- .../StakingActionSelectorBottomSheet.kt | 4 ++-- ...TokenDetailsBalanceSelectStateConverter.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 2 +- .../TokenDetailsStakingInfoConverter.kt | 4 ++-- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- 86 files changed, 183 insertions(+), 154 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/local/token/converter/{TokenConverter.kt => YieldTokenConverter.kt} (77%) rename domain/{staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit => models/src/main/kotlin/com/tangem/domain/models/staking}/NetworkType.kt (90%) rename domain/{staking/models/src/main/kotlin/com/tangem/domain/staking/model => models/src/main/kotlin/com/tangem/domain/models/staking}/StakingID.kt (75%) rename domain/{staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit => models/src/main/kotlin/com/tangem/domain/models/staking}/YieldBalance.kt (95%) rename domain/{staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit => models/src/main/kotlin/com/tangem/domain/models/staking}/YieldBalanceItem.kt (95%) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt rename domain/{staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit => models/src/main/kotlin/com/tangem/domain/models/staking}/action/StakingActionType.kt (91%) diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt index 1616a4c847..bb2343344e 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldBalanceWrapperDTOFactory.kt @@ -5,7 +5,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import java.math.BigDecimal /** diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt index 189705d948..c58ff8edc0 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import java.math.BigDecimal /** diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 1ddbfffe90..da96ebf46d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.StringsSigns.DASH_SIGN diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index efd6c85900..c7ab58a7fd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType @JsonClass(generateAdapter = true) data class PendingActionRequestBody( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt index a1e9edcb49..772c349e0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.staking.BalanceType import com.tangem.utils.converter.Converter internal object BalanceTypeConverter : Converter { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt index 2284023080..129ecf3eda 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.PendingActionConstraints +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.PendingActionConstraints import com.tangem.utils.converter.Converter internal object PendingActionConstraintsConverter : diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt index 716d5fc960..161bc9885c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.utils.converter.Converter internal object PendingActionConverter : Converter { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt index d95777148f..f5300b3327 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.utils.converter.Converter @Suppress("CyclomaticComplexMethod") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt index e69821a778..adc411b5e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.utils.converter.TwoWayConverter @Suppress("CyclomaticComplexMethod", "LongMethod") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 5d86e9a0be..828449590f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -3,10 +3,10 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceItem +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.YieldBalanceItem import com.tangem.utils.converter.Converter import kotlinx.datetime.Instant @@ -43,7 +43,7 @@ class YieldBalanceConverter( return BalanceItem( groupId = item.groupId, - token = TokenConverter.convert(item.tokenDTO), + token = YieldTokenConverter.convert(item.tokenDTO), type = BalanceTypeConverter.convert(item.type), amount = item.amount, rawCurrencyId = item.tokenDTO.coinGeckoId, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt similarity index 77% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt index 4a363aa324..4c4e1934df 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldTokenConverter.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken import com.tangem.utils.converter.TwoWayConverter -object TokenConverter : TwoWayConverter { +object YieldTokenConverter : TwoWayConverter { - override fun convert(value: TokenDTO): Token { - return Token( + override fun convert(value: TokenDTO): YieldToken { + return YieldToken( name = value.name, network = StakingNetworkTypeConverter.convert(value.network), symbol = value.symbol, @@ -19,7 +19,7 @@ object TokenConverter : TwoWayConverter { ) } - override fun convertBack(value: Token): TokenDTO { + override fun convertBack(value: YieldToken): TokenDTO { return TokenDTO( name = value.name, network = StakingNetworkTypeConverter.convertBack(value.network), diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index dfb797e129..0dad5046fe 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -29,22 +29,22 @@ import com.tangem.datasource.api.stakekit.models.response.model.action.StakingAc import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction @@ -399,7 +399,7 @@ internal class DefaultStakingRepository( ), args = ActionRequestBodyArgs( amount = params.amount.toPlainString(), - inputToken = TokenConverter.convertBack(params.token), + inputToken = YieldTokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, validatorAddresses = listOf(params.validatorAddress), // check on other networks tronResource = getTronResource(network), diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 9d8c056e83..60fe5f9e3b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule @@ -24,8 +24,8 @@ internal object YieldConverter : Converter { override fun convert(value: YieldDTO): Yield { return Yield( id = value.id.asMandatory("id"), - token = TokenConverter.convert(value.token.asMandatory("token")), - tokens = value.tokens.asMandatory("tokens").map(TokenConverter::convert), + token = YieldTokenConverter.convert(value.token.asMandatory("token")), + tokens = value.tokens.asMandatory("tokens").map(YieldTokenConverter::convert), args = convertArgs(value.args.asMandatory("args")), status = convertStatus(value.status.asMandatory("status")), apy = value.apy.asMandatory("apy"), @@ -91,9 +91,9 @@ internal object YieldConverter : Converter { logoUri = metadataDTO.logoUri.asMandatory("logoUri"), description = metadataDTO.description.asMandatory("description"), documentation = metadataDTO.documentation, - gasFeeToken = TokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), - token = TokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), - tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(TokenConverter::convert), + gasFeeToken = YieldTokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), + token = YieldTokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), + tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(YieldTokenConverter::convert), type = metadataDTO.type.asMandatory("type"), rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")), cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) }, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt index a9fb76fc0a..64ed35b3e1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO -import com.tangem.datasource.local.token.converter.TokenConverter +import com.tangem.datasource.local.token.converter.YieldTokenConverter import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.utils.converter.Converter @@ -10,7 +10,7 @@ internal object GasEstimateConverter : Converter().configureEach { } dependencies { + api(projects.domain.core) api(projects.domain.visa.models) api(projects.core.utils) @@ -19,6 +20,7 @@ dependencies { implementation(tangemDeps.hot.core) implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) + implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) ksp(deps.moshi.kotlin.codegen) implementation(deps.arrow.core) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt similarity index 90% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt index 2733a30688..3e0921c351 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/NetworkType.kt @@ -1,5 +1,8 @@ -package com.tangem.domain.staking.model.stakekit +package com.tangem.domain.models.staking +import kotlinx.serialization.Serializable + +@Serializable enum class NetworkType { AVALANCHE_C, AVALANCHE_ATOMIC, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt similarity index 75% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt index 59618e15b0..5ed3a0b3ce 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/StakingID.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.models.staking import kotlinx.serialization.Serializable diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt similarity index 95% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt index 1dd03af2b9..3badeed6e2 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalance.kt @@ -1,7 +1,6 @@ -package com.tangem.domain.staking.model.stakekit +package com.tangem.domain.models.staking import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.StakingID import kotlinx.serialization.Serializable /** diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt similarity index 95% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt index b5bbd086b5..0f59dfe679 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceItem.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt @@ -1,7 +1,7 @@ -package com.tangem.domain.staking.model.stakekit +package com.tangem.domain.models.staking import com.tangem.domain.core.serialization.SerializedBigDecimal -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import kotlinx.datetime.Instant import kotlinx.serialization.Serializable @@ -14,7 +14,7 @@ data class YieldBalanceItem( @Serializable data class BalanceItem( val groupId: String, - val token: Token, + val token: YieldToken, val type: BalanceType, val amount: SerializedBigDecimal, val rawCurrencyId: String?, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt new file mode 100644 index 0000000000..18954a5c1f --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldToken.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.models.staking + +import kotlinx.serialization.Serializable + +@Serializable +data class YieldToken( + val name: String, + val network: NetworkType, + val symbol: String, + val decimals: Int, + val address: String?, + val coinGeckoId: String?, + val logoURI: String?, + val isPoints: Boolean?, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt similarity index 91% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt index 467bb051aa..d255b23852 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/action/StakingActionType.kt @@ -1,5 +1,8 @@ -package com.tangem.domain.staking.model.stakekit.action +package com.tangem.domain.models.staking.action +import kotlinx.serialization.Serializable + +@Serializable enum class StakingActionType { STAKE, UNSTAKE, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 063bcce89a..75a9e20b8f 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -1,13 +1,14 @@ package com.tangem.domain.staking.model.stakekit import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.staking.YieldToken import kotlinx.serialization.Serializable @Serializable data class Yield( val id: String, - val token: Token, - val tokens: List, + val token: YieldToken, + val tokens: List, val args: Args, val status: Status, val apy: SerializedBigDecimal, @@ -93,9 +94,9 @@ data class Yield( val logoUri: String, val description: String, val documentation: String?, - val gasFeeToken: Token, - val token: Token, - val tokens: List, + val gasFeeToken: YieldToken, + val token: YieldToken, + val tokens: List, val type: String, val rewardSchedule: RewardSchedule, val cooldownPeriod: Period?, @@ -145,18 +146,6 @@ data class Yield( } } -@Serializable -data class Token( - val name: String, - val network: NetworkType, - val symbol: String, - val decimals: Int, - val address: String?, - val coinGeckoId: String?, - val logoURI: String?, - val isPoints: Boolean?, -) - @Serializable data class AddressArgument( val required: Boolean, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt index be1545d6da..bfe0503203 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt @@ -1,5 +1,6 @@ package com.tangem.domain.staking.model.stakekit.action +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import org.joda.time.DateTime import java.math.BigDecimal diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt index 605ed69fa5..1bf184eded 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt @@ -1,8 +1,8 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import java.math.BigDecimal data class ActionParams( @@ -11,7 +11,7 @@ data class ActionParams( val amount: BigDecimal, val address: String, val validatorAddress: String, - val token: Token, + val token: YieldToken, val publicKey: String? = null, val passthrough: String? = null, val type: StakingActionType? = null, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt index 585ae1edb5..7f8562f492 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt @@ -1,10 +1,10 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.models.staking.YieldToken import java.math.BigDecimal data class StakingGasEstimate( val amount: BigDecimal, - val token: Token, + val token: YieldToken, val gasLimit: String?, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt index 4222203a21..058a7d1e6d 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType data class StakingTransaction( val id: String, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt index bf3fb63499..f44adef4ae 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.repositories.StakingActionRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt index ccbb011f35..c3c236025c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import java.math.BigDecimal class GetActionRequirementAmountUseCase { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 2b1a377476..f24cac1f74 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -1,10 +1,14 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.YieldToken +import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.utils.extensions.isEqualTo import java.math.BigDecimal @@ -17,7 +21,7 @@ class InvalidatePendingTransactionsUseCase( operator fun invoke( balanceItems: List, stakingActions: List, - token: Token, + token: YieldToken, ): Either> { return Either.catch { val balancesToDisplay = mergeBalancesAndProcessingActions( @@ -34,7 +38,7 @@ class InvalidatePendingTransactionsUseCase( private fun mergeBalancesAndProcessingActions( realBalances: List, processingActions: List, - token: Token, + token: YieldToken, ): List { val balances = realBalances.toMutableList() @@ -87,7 +91,7 @@ class InvalidatePendingTransactionsUseCase( private fun addStubStakedPendingTransaction( balances: MutableList, action: StakingAction, - token: Token, + token: YieldToken, ) { balances.add( BalanceItem( @@ -153,7 +157,7 @@ class InvalidatePendingTransactionsUseCase( return index to action.amount } - private fun doPostProcessing(balances: MutableList, action: StakingAction, token: Token) { + private fun doPostProcessing(balances: MutableList, action: StakingAction, token: YieldToken) { val validatorAddress = action.validatorAddress ?: action.validatorAddresses?.firstOrNull() if (token.network == NetworkType.TON && validatorAddress != null) { for (index in balances.indices) { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt index a830af4484..c8648ad64c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt @@ -6,7 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.walletmanager.WalletManagersFacade diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index 063e0e9f70..f8376e3707 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -4,7 +4,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.staking.analytics.StakingAnalyticsEvent.ButtonRewards.addIfValueIsNotNull import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType sealed class StakingAnalyticsEvent( event: String, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt index 17330db5b0..041755dbc8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID /** * Fetcher of yields balances diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt index b5b2446198..7b9357cb24 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceProducer.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt index 4c6cbed1f3..6d6f106113 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceSupplier.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.multi import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance /** * Supplier of all yield balances for selected wallet [MultiYieldBalanceProducer.Params] diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 3be0818547..efbbae2c38 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt index 789c93584a..61a7ee90c6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID /** * Fetcher of yield balance diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt index 7fea7e7014..9d33907751 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt @@ -2,8 +2,8 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance /** * Producer of yield balance for selected wallet [UserWalletId] diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt index f7509dc980..4d93e733a1 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.single import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.core.flow.FlowProducer -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance /** * Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params] diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt index fc7a759b50..f6b6a96d42 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking.utils -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.lib.crypto.BlockchainUtils import java.math.BigDecimal diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index cd77c25e9e..09f2670173 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -9,7 +9,7 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.ProvideTestModels import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.walletmanager.WalletManagersFacade import io.mockk.clearMocks diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 65d534c54b..3fc15a0f52 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -5,7 +5,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import java.math.BigDecimal /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 70f695f4ef..abf478099e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -17,9 +17,9 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.isStakingSupported -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 797a76ca5f..9d0dfe2461 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -25,9 +25,9 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index ca1b7b0fcb..da8de5018f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -4,7 +4,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index ec1e5a826d..2bd4ff898a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -4,7 +4,7 @@ import arrow.core.NonEmptyList import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.getResultStatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.lib.crypto.BlockchainUtils diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 72382a701c..bec82f8e2a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -10,7 +10,7 @@ import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index 53b7db96ff..08138e4e05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -7,9 +7,9 @@ import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index f7cc8a5633..1a8e1bf3f0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 3c56d06c66..65ffcfdedb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 96e5279cb3..90a4867e0c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -29,6 +29,11 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.RewardBlockType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -40,7 +45,7 @@ import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index a4b3ea96fd..36a4a319f5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -2,6 +2,10 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.PendingActionConstraints +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.staking.model.stakekit.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index e4bc6089b6..3f8262760b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt index 873ba40422..6601a859f6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingActionSelectionBottomSheetConfig.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.bottomsheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction internal data class StakingActionSelectionBottomSheetConfig( val title: TextReference, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index bdc0f43c77..9476521ace 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -6,12 +6,12 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.BalanceType.Companion.isClickable +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.BalanceType.Companion.isClickable import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 4fefa8ca21..af1a4bde08 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -6,10 +6,10 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index f82c1ece1f..0ed2a3814c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -5,8 +5,12 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.staking.BalanceItem +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.stakekit.* -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index fe8e42f2a8..94e68d2aad 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -9,7 +9,7 @@ import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.staking.EstimateGasUseCase -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index 74cfa8e53b..f2a1574d06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -10,8 +10,8 @@ import com.tangem.domain.staking.GetStakingTransactionsUseCase import com.tangem.domain.staking.SaveUnsubmittedHashUseCase import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.SubmitHashData -import com.tangem.domain.staking.model.stakekit.NetworkType -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index d7af6496e2..da70fc8533 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.RewardBlockType +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index ba40e2960c..99b2bd7041 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 82f48a612c..e2e24ff112 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt index c70861712b..07077a8575 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowActionSelectorBottomSheetTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.models.staking.PendingAction import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 10e064d094..ee2c810e32 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -4,11 +4,11 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index f9f71dd51e..d9948ed3b8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.validat import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index da281e339d..6ef20b93a9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isCardano diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 1cccaf852d..462c5d22c0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -42,8 +42,8 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.RewardBlockType +import com.tangem.domain.models.staking.BalanceType +import com.tangem.domain.models.staking.RewardBlockType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.BalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt index 9a1c63bc5b..ae9fc0d984 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -17,8 +17,8 @@ import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index ac4af1dc72..c2f97a9c09 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -4,7 +4,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.* diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 75fd755d3b..da60bfba2b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 82a04112f4..2a7dfc2ef6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -10,8 +10,8 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.RewardBlockType -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.RewardBlockType +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 87c169926c..037bed6176 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem From 011df1a797df1bf6ad2ab2be2e5608705c29836a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 14:49:45 +0400 Subject: [PATCH 019/165] Updated on 2026-08-14 --- .../exchangeServices/DefaultRampManager.kt | 2 +- .../converters/AmountCurrencyTransformer.kt | 2 +- .../converters/AmountReduceByTransformer.kt | 2 +- .../converters/AmountReduceToTransformer.kt | 2 +- .../converters/AmountStateConverter.kt | 2 +- .../converters/MaxEnterAmountConverter.kt | 2 +- .../field/AmountBoundaryUpdateTransformer.kt | 2 +- .../field/AmountFieldChangeTransformer.kt | 2 +- .../converters/field/AmountFieldConverter.kt | 2 +- .../AmountFieldSetMaxAmountTransformer.kt | 2 +- .../ui/notifications/NotificationsFactory.kt | 2 +- .../ui/tokens/TokenItemStateConverter.kt | 2 +- .../CryptoCurrencyToIconStateConverter.kt | 2 +- .../onramp/models/OnrampTransactionDTO.kt | 2 +- .../data/swap/DefaultSwapRepositoryV2.kt | 2 +- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../DefaultCurrencyChecksRepository.kt | 2 +- .../domain/exchange/RampStateManager.kt | 2 +- .../domain/markets/TokenMarketParams.kt | 2 +- .../models/currency}/CryptoCurrencyStatus.kt | 117 ++++++++++++------ .../domain/models/network/NetworkAddress.kt | 6 + .../tangem/domain/models/network/TxInfo.kt | 58 ++++++++- .../serialization/BigDecimalSerializer.kt | 2 +- .../serialization/BigIntegerSerializer.kt | 2 +- .../serialization/SerializedBigDecimal.kt | 2 +- .../serialization/SerializedBigInteger.kt | 2 +- .../domain/models/staking/YieldBalanceItem.kt | 2 +- .../com/tangem/domain/nft/models/NFTAsset.kt | 2 +- .../tangem/domain/nft/models/NFTSalePrice.kt | 2 +- .../domain/onramp/model/OnrampAmount.kt | 2 +- .../onramp/model/cache/OnrampTransaction.kt | 4 +- .../domain/onramp/GetLegacyTopUpUrlUseCase.kt | 4 +- .../domain/staking/model/stakekit/Yield.kt | 2 +- .../domain/swap/models/SwapCurrencies.kt | 2 +- .../domain/swap/models/SwapPairModel.kt | 2 +- .../tangem/domain/swap/SwapRepositoryV2.kt | 2 +- .../domain/swap/usecase/GetSwapDataUseCase.kt | 2 +- .../swap/usecase/GetSwapPairsUseCase.kt | 4 +- .../usecase/GetSwapSupportedPairsUseCase.kt | 2 +- .../swap/usecase/SelectInitialPairUseCase.kt | 2 +- .../usecase/SwapTransactionSentUseCase.kt | 4 +- ...AllWalletsCryptoCurrencyStatusesUseCase.kt | 4 +- ...GetBalanceNotEnoughForFeeWarningUseCase.kt | 4 +- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 6 +- .../domain/tokens/GetCurrencyCheckUseCase.kt | 4 +- .../tokens/GetCurrencyWarningsUseCase.kt | 4 +- ...tFeePaidCryptoCurrencyStatusSyncUseCase.kt | 4 +- .../GetMinimumTransactionAmountSyncUseCase.kt | 4 +- .../GetMultiCryptoCurrencyStatusUseCase.kt | 4 +- .../tokens/GetNetworkCoinStatusUseCase.kt | 10 +- .../GetSingleCryptoCurrencyStatusUseCase.kt | 4 +- .../domain/tokens/GetTokenListUseCase.kt | 4 +- .../tokens/GetWalletTotalBalanceUseCase.kt | 2 +- .../tokens/actions/BaseActionsFactory.kt | 2 +- .../tokens/actions/CommonActionsFactory.kt | 6 +- .../actions/OutdatedDataActionsFactory.kt | 4 +- .../actions/UnreachableActionsFactory.kt | 4 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 2 +- .../domain/tokens/model/NetworkGroup.kt | 1 + .../domain/tokens/model/TokenActionsState.kt | 3 +- .../tangem/domain/tokens/model/TokenList.kt | 1 + .../BaseCurrenciesStatusesOperations.kt | 2 +- .../BaseCurrencyStatusOperations.kt | 6 +- .../CachedCurrenciesStatusesOperations.kt | 6 +- .../operations/CurrencyStatusOperations.kt | 2 +- .../TokenListFiatBalanceOperations.kt | 2 +- .../tokens/operations/TokenListOperations.kt | 2 +- .../operations/TokenListSortingOperations.kt | 2 +- .../tokens/repository/CurrenciesRepository.kt | 4 +- .../repository/CurrencyChecksRepository.kt | 4 +- .../utils/CurrencyStatusProxyCreator.kt | 4 +- .../domain/tokens/mock/MockTokenLists.kt | 2 +- .../domain/tokens/mock/MockTokensStates.kt | 2 +- .../repository/MockCurrenciesRepository.kt | 4 +- .../portfolio/impl/loader/PortfolioData.kt | 2 +- .../impl/model/MyPortfolioUMFactory.kt | 2 +- .../impl/model/PortfolioTokenUMConverter.kt | 4 +- .../impl/model/TokensPortfolioUMConverter.kt | 2 +- .../topup/model/OnboardingNoteTopUpModel.kt | 4 +- .../impl/model/OnboardingNoteCommonState.kt | 2 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 4 +- .../onramp/hottokens/HotCryptoComponent.kt | 2 +- .../selecttoken/model/OnrampOperationModel.kt | 4 +- .../AvailableSwapPairsComponent.kt | 2 +- .../LoadingTokenListItemConverter.kt | 2 +- .../SetLoadingTokenItemsTransformer.kt | 2 +- .../SetNoAvailablePairsTransformer.kt | 2 +- .../model/AvailableSwapPairsModel.kt | 2 +- .../swap/model/SwapSelectTokensModel.kt | 2 +- .../tokenlist/OnrampTokenListComponent.kt | 2 +- .../UpdateTokenItemsTransformer.kt | 2 +- .../OnrampTokenItemStateConverterFactory.kt | 2 +- .../tokenlist/model/OnrampTokenListModel.kt | 2 +- .../send/v2/api/SendNotificationsComponent.kt | 4 +- .../send/v2/api/params/FeeSelectorParams.kt | 2 +- .../feeSelector/utils/FeeCalculationUtils.kt | 2 +- .../utils/FeeCalculationUtilsTest.kt | 2 +- .../model/transformers/FeeItemConverter.kt | 2 +- .../FeeSelectorCustomFieldConverter.kt | 2 +- ...eeSelectorCustomValueChangedTransformer.kt | 2 +- .../FeeSelectorLoadedTransformer.kt | 2 +- .../send/v2/send/DefaultSendComponent.kt | 2 +- .../v2/send/confirm/SendConfirmComponent.kt | 4 +- .../features/send/v2/send/model/SendModel.kt | 2 +- .../confirm/NFTSendConfirmComponent.kt | 10 +- .../send/v2/sendnft/model/NFTSendModel.kt | 2 +- .../amount/SendAmountComponentParams.kt | 4 +- .../amount/model/SendAmountModel.kt | 2 +- .../fee/SendFeeComponentParams.kt | 4 +- .../subcomponents/fee/model/FeeCalculation.kt | 2 +- .../fee/model/converters/FeeConverter.kt | 4 +- .../converters/SendFeeCustomFieldConverter.kt | 4 +- .../bitcoin/BitcoinCustomFeeConverter.kt | 2 +- .../ethereum/EthereumCustomFeeConverter.kt | 4 +- .../ethereum/EthereumEIPCustomFeeConverter.kt | 4 +- .../EthereumLegacyCustomFeeConverter.kt | 4 +- .../custom/kaspa/KaspaCustomFeeConverter.kt | 4 +- .../SendFeeCustomAutoFixTransformer.kt | 6 +- .../SendFeeCustomValueChangeTransformer.kt | 6 +- .../SendFeeInitialStateTransformer.kt | 2 +- .../transformers/SendFeeLoadedTransformer.kt | 2 +- .../transformers/SendFeeSelectTransformer.kt | 6 +- .../notifications/model/NotificationsModel.kt | 2 +- .../analytics/utils/StakingAnalyticSender.kt | 4 +- .../impl/presentation/model/StakingModel.kt | 13 +- .../state/converters/BalanceItemConverter.kt | 4 +- .../RewardsValidatorStateConverter.kt | 4 +- .../converters/YieldBalancesConverter.kt | 4 +- .../state/helpers/StakingBalanceUpdater.kt | 4 +- .../helpers/StakingFeeTransactionLoader.kt | 6 +- .../state/helpers/StakingTransactionSender.kt | 8 +- .../transformers/SetAmountDataTransformer.kt | 6 +- .../SetConfirmationStateAssentTransformer.kt | 2 +- ...etConfirmationStateCompletedTransformer.kt | 2 +- .../SetConfirmationStateInitTransformer.kt | 6 +- ...ConfirmationStateResetAssentTransformer.kt | 2 +- .../SetInitialDataStateTransformer.kt | 4 +- .../amount/AmountChangeStateTransformer.kt | 2 +- .../AmountCurrencyChangeStateTransformer.kt | 2 +- .../amount/AmountMaxValueStateTransformer.kt | 2 +- .../amount/AmountReduceByStateTransformer.kt | 2 +- .../amount/AmountReduceToStateTransformer.kt | 2 +- .../AmountRequirementStateTransformer.kt | 2 +- .../amount/AmountRoundToIntegerTransformer.kt | 2 +- ...firmationStateAssentApprovalTransformer.kt | 2 +- .../ShowApprovalBottomSheetTransformer.kt | 2 +- .../AddStakingNotificationsTransformer.kt | 2 +- .../StakingInfoNotificationsFactory.kt | 6 +- .../state/utils/FeeCalculation.kt | 2 +- .../impl/amount/SwapAmountComponentParams.kt | 4 +- .../v2/impl/amount/entity/SwapAmountUM.kt | 2 +- .../v2/impl/amount/model/SwapAmountModel.kt | 2 +- .../impl/amount/model/SwapAmountQuoteUtils.kt | 2 +- .../converter/SwapAmountFieldConverter.kt | 2 +- .../SwapAmountPrimaryReadyStateTransformer.kt | 4 +- ...wapAmountSecondaryReadyStateTransformer.kt | 4 +- .../SwapAmountUpdateBalanceTransformer.kt | 2 +- .../ui/preview/SwapAmountContentPreview.kt | 2 +- .../swap/v2/impl/common/ConfirmData.kt | 2 +- .../confirm/SendWithSwapConfirmComponent.kt | 2 +- .../confirm/model/SendWithSwapConfirmModel.kt | 2 +- .../confirm/model/SwapTransactionSender.kt | 2 +- .../sendviaswap/model/SendWithSwapModel.kt | 2 +- .../domain/models/domain/SwapPairLeast.kt | 2 +- .../swap/domain/models/ui/SwapState.kt | 2 +- .../DefaultInitialToCurrencyResolver.kt | 2 +- .../swap/domain/InitialToCurrencyResolver.kt | 2 +- .../feature/swap/domain/SwapInteractor.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 6 +- .../swap/converters/TokensDataConverter.kt | 2 +- .../tangem/feature/swap/model/SwapModel.kt | 8 +- .../swap/model/SwapNotificationsFactory.kt | 2 +- .../swap/model/SwapProcessDataState.kt | 2 +- .../feature/swap/models/SwapStateHolder.kt | 2 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- ...kenDetailsCurrencyStatusAnalyticsSender.kt | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 2 +- ...TokenDetailsBalanceSelectStateConverter.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 2 +- ...nDetailsOnrampTransactionStateConverter.kt | 2 +- .../TokenDetailsStakingInfoConverter.kt | 6 +- .../state/factory/TokenDetailsStateFactory.kt | 2 +- .../factory/express/ExpressStatusFactory.kt | 4 +- .../factory/express/OnrampStatusFactory.kt | 4 +- .../txhistory/model/TxHistoryModel.kt | 4 +- .../intents/WalletContentClickIntents.kt | 6 +- .../WalletCurrencyActionsClickIntents.kt | 6 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- .../router/DefaultWalletRouter.kt | 6 +- .../presentation/router/InnerWalletRouter.kt | 2 +- .../utils/TokenListAnalyticsSender.kt | 2 +- .../domain/GetMultiWalletWarningsFactory.kt | 2 +- .../domain/GetSingleWalletWarningsFactory.kt | 6 +- .../presentation/wallet/domain/UseCaseExt.kt | 6 +- .../wallet/domain/WalletWithFundsChecker.kt | 2 +- .../SetExpressStatusesTransformer.kt | 8 +- .../SetPrimaryCurrencyTransformer.kt | 2 +- .../transformers/SetVisaInfoTransformer.kt | 10 +- .../MultiWalletCurrencyActionsConverter.kt | 8 +- .../SingleWalletCardStateConverter.kt | 2 +- .../SingleWalletMarketPriceConverter.kt | 2 +- .../SingleWalletOnrampTransactionConverter.kt | 2 +- .../converter/TokenListStateConverter.kt | 2 +- .../subscribers/PrimaryCurrencySubscriber.kt | 4 +- .../SingleWalletExpressStatusesSubscriber.kt | 4 +- .../wallet/subscribers/TxHistorySubscriber.kt | 4 +- .../model/WcSendTransactionModel.kt | 2 +- 207 files changed, 453 insertions(+), 362 deletions(-) rename domain/{tokens/models/src/main/java/com/tangem/domain/tokens/model => models/src/main/kotlin/com/tangem/domain/models/currency}/CryptoCurrencyStatus.kt (68%) rename domain/{core/src/main/kotlin/com/tangem/domain/core => models/src/main/kotlin/com/tangem/domain/models}/serialization/BigDecimalSerializer.kt (93%) rename domain/{core/src/main/kotlin/com/tangem/domain/core => models/src/main/kotlin/com/tangem/domain/models}/serialization/BigIntegerSerializer.kt (93%) rename domain/{core/src/main/kotlin/com/tangem/domain/core => models/src/main/kotlin/com/tangem/domain/models}/serialization/SerializedBigDecimal.kt (77%) rename domain/{core/src/main/kotlin/com/tangem/domain/core => models/src/main/kotlin/com/tangem/domain/models}/serialization/SerializedBigInteger.kt (77%) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index be5a5a9705..bfeee05b09 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -13,9 +13,9 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.utils.Provider diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt index cdacdd3ce7..7406013c29 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index e52898094a..244ea5b4c0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index 7e5cf0ab8d..cb91187d95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index a38f516c70..937f94852b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt index dce1957361..045c704421 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt @@ -1,7 +1,7 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index a689b4501a..8761c5abb0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.transformer.Transformer /** diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index cbfc6907e8..4a490037ae 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index f19fad6bb4..9c42eeac7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -9,9 +9,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.convertToAmount import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 72dcac8ac9..a8c20c0745 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.extensions.isZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 5e3fb48b92..73f9baa570 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index da96ebf46d..9c80a6a1b7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -13,9 +13,9 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isZero diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt index be2a3778ff..152ff9caf6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt @@ -5,7 +5,7 @@ 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.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt index 582fbead56..95beb54a57 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/models/OnrampTransactionDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.datasource.api.onramp.models.response.Status import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 51f04d6a97..46346745a5 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -22,6 +22,7 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher @@ -29,7 +30,6 @@ import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 6687fab2f7..190429d257 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -22,9 +22,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index db4a2cabb1..f374404cbe 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -7,11 +7,11 @@ import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.utils.getTotalStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index dd4d2ae6d7..3ac8a33c7f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -3,9 +3,9 @@ package com.tangem.domain.exchange import arrow.core.Either import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import kotlinx.coroutines.flow.Flow diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt index 12ab5b67e1..01cff0386d 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketParams.kt @@ -1,7 +1,7 @@ package com.tangem.domain.markets -import com.tangem.domain.core.serialization.SerializedBigDecimal import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt similarity index 68% rename from domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt index 3fc15a0f52..64e478276e 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyStatus.kt @@ -1,12 +1,12 @@ -package com.tangem.domain.tokens.model +package com.tangem.domain.models.currency import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.staking.YieldBalance -import java.math.BigDecimal +import kotlinx.serialization.Serializable /** * Represents the status of a cryptocurrency asset within a network. @@ -18,6 +18,7 @@ import java.math.BigDecimal * @property currency The details of the cryptocurrency asset, including its type, name, symbol, and other properties. * @property value The current status of the cryptocurrency, reflecting its state within the network. */ +@Serializable data class CryptoCurrencyStatus( val currency: CryptoCurrency, val value: Value, @@ -28,36 +29,40 @@ data class CryptoCurrencyStatus( * * @property isError Indicates whether this status represents an error status. */ - sealed class Value(val isError: Boolean) { + @Serializable + sealed interface Value { + + val isError: Boolean /** The amount of the cryptocurrency. */ - open val amount: BigDecimal? = null + val amount: SerializedBigDecimal? get() = null /** The fiat equivalent of the cryptocurrency's amount. */ - open val fiatAmount: BigDecimal? = null + val fiatAmount: SerializedBigDecimal? get() = null /** The exchange rate used for converting the cryptocurrency amount to fiat. */ - open val fiatRate: BigDecimal? = null + val fiatRate: SerializedBigDecimal? get() = null /** The change in price of the cryptocurrency. */ - open val priceChange: BigDecimal? = null + val priceChange: SerializedBigDecimal? get() = null /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ - open val hasCurrentNetworkTransactions: Boolean = false + val hasCurrentNetworkTransactions: Boolean get() = false /** The pending cryptocurrency transactions. */ - open val pendingTransactions: Set = emptySet() + val pendingTransactions: Set get() = emptySet() /** The network address */ - open val networkAddress: NetworkAddress? = null + val networkAddress: NetworkAddress? get() = null /** Staking yield balance */ - open val yieldBalance: YieldBalance? = null + val yieldBalance: YieldBalance? get() = null /** Sources */ - open val sources: Sources = Sources() + val sources: Sources get() = Sources() } + @Serializable data class Sources( val networkSource: StatusSource = StatusSource.ACTUAL, val quoteSource: StatusSource = StatusSource.ACTUAL, @@ -70,7 +75,11 @@ data class CryptoCurrencyStatus( } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ - data object Loading : Value(isError = false) + @Serializable + data object Loading : Value { + + override val isError: Boolean = false + } /** * Represents a state where the cryptocurrency is not reachable. @@ -79,23 +88,35 @@ data class CryptoCurrencyStatus( * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat. * @property networkAddress The network address */ + @Serializable data class Unreachable( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, override val networkAddress: NetworkAddress?, - ) : Value(isError = true) + ) : Value { + + override val isError: Boolean = true + } /** Represents a state where the cryptocurrency's network amount not found. */ + @Serializable data class NoAmount( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, - ) : Value(isError = true) + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + ) : Value { + + override val isError: Boolean = true + } /** Represents a state where the cryptocurrency's derivation is missed. */ + @Serializable data class MissedDerivation( - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, - ) : Value(isError = true) + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + ) : Value { + + override val isError: Boolean = true + } /** * Represents a state where there is no account associated with the cryptocurrency @@ -103,16 +124,18 @@ data class CryptoCurrencyStatus( * @property amountToCreateAccount base reserve amount for account creation * @property sources sources of data */ + @Serializable data class NoAccount( - val amountToCreateAccount: BigDecimal, - override val fiatAmount: BigDecimal?, - override val priceChange: BigDecimal?, - override val fiatRate: BigDecimal?, + val amountToCreateAccount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal?, + override val priceChange: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) { + ) : Value { - override val amount: BigDecimal = BigDecimal.ZERO + override val isError: Boolean = false + override val amount: SerializedBigDecimal? = SerializedBigDecimal.ZERO } /** @@ -127,17 +150,21 @@ data class CryptoCurrencyStatus( * @property pendingTransactions The current cryptocurrency transactions. * @property sources sources of data */ + @Serializable data class Loaded( - override val amount: BigDecimal, - override val fiatAmount: BigDecimal, - override val fiatRate: BigDecimal, - override val priceChange: BigDecimal, + override val amount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal, + override val fiatRate: SerializedBigDecimal, + override val priceChange: SerializedBigDecimal, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } /** * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. @@ -150,17 +177,21 @@ data class CryptoCurrencyStatus( * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. */ + @Serializable data class Custom( - override val amount: BigDecimal, - override val fiatAmount: BigDecimal?, - override val fiatRate: BigDecimal?, - override val priceChange: BigDecimal?, + override val amount: SerializedBigDecimal, + override val fiatAmount: SerializedBigDecimal?, + override val fiatRate: SerializedBigDecimal?, + override val priceChange: SerializedBigDecimal?, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } /** * Represents a state where the cryptocurrency is available, but there is no current quote available for it. @@ -170,12 +201,16 @@ data class CryptoCurrencyStatus( * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. */ + @Serializable data class NoQuote( - override val amount: BigDecimal, + override val amount: SerializedBigDecimal, override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, override val sources: Sources, - ) : Value(isError = false) + ) : Value { + + override val isError: Boolean = false + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt index 0fef921e21..f32f27b752 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/NetworkAddress.kt @@ -1,6 +1,9 @@ package com.tangem.domain.models.network +import kotlinx.serialization.Serializable + /** Represents a network address */ +@Serializable sealed class NetworkAddress { /** The default or currently selected network address */ @@ -14,6 +17,7 @@ sealed class NetworkAddress { * * @property defaultAddress the static network address */ + @Serializable data class Single(override val defaultAddress: Address) : NetworkAddress() { override val availableAddresses: Set
= setOf(defaultAddress) @@ -25,6 +29,7 @@ sealed class NetworkAddress { * @property defaultAddress the currently selected or default network address * @property availableAddresses the set of available network addresses to choose from */ + @Serializable data class Selectable( override val defaultAddress: Address, override val availableAddresses: Set
, @@ -41,6 +46,7 @@ sealed class NetworkAddress { * @property value string representation of the address * @property type address type */ + @Serializable data class Address( val value: String, val type: Type, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt index f2d47d0f00..d5bb077099 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/TxInfo.kt @@ -1,6 +1,7 @@ package com.tangem.domain.models.network -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable /** * Represents information about a transaction. Do not use it for sending transactions. @@ -15,6 +16,7 @@ import java.math.BigDecimal * @property type transaction type * @property amount transaction amount */ +@Serializable data class TxInfo( val txHash: String, val timestampInMillis: Long, @@ -24,10 +26,11 @@ data class TxInfo( val interactionAddressType: InteractionAddressType?, val status: TransactionStatus, val type: TransactionType, - val amount: BigDecimal, + val amount: SerializedBigDecimal, ) { /** Destination type*/ + @Serializable sealed class DestinationType { /** @@ -35,6 +38,7 @@ data class TxInfo( * * @property addressType address type */ + @Serializable data class Single(val addressType: AddressType) : DestinationType() /** @@ -42,21 +46,29 @@ data class TxInfo( * * @property addressTypes addresses types */ + @Serializable data class Multiple(val addressTypes: List) : DestinationType() } /** Address type */ + @Serializable sealed class AddressType { /** Address value */ abstract val address: String + @Serializable data class User(override val address: String) : AddressType() + + @Serializable data class Contract(override val address: String) : AddressType() + + @Serializable data class Validator(override val address: String) : AddressType() } /** Source type */ + @Serializable sealed class SourceType { /** @@ -64,6 +76,7 @@ data class TxInfo( * * @property address address */ + @Serializable data class Single(val address: String) : SourceType() /** @@ -71,38 +84,79 @@ data class TxInfo( * * @property addresses addresses */ + @Serializable data class Multiple(val addresses: List) : SourceType() } /** Transaction type */ + @Serializable sealed interface TransactionType { + + @Serializable data object Transfer : TransactionType + + @Serializable data object Approve : TransactionType + + @Serializable data object Swap : TransactionType + + @Serializable data object UnknownOperation : TransactionType + + @Serializable data class Operation(val name: String) : TransactionType + @Serializable sealed interface Staking : TransactionType { + + @Serializable data class Vote(val validatorAddress: String) : Staking + + @Serializable data object ClaimRewards : Staking + + @Serializable data object Stake : Staking + + @Serializable data object Unstake : Staking + + @Serializable data object Withdraw : Staking + + @Serializable data object Restake : Staking } } /** Transaction status */ + @Serializable sealed class TransactionStatus { + + @Serializable data object Failed : TransactionStatus() + + @Serializable data object Unconfirmed : TransactionStatus() + + @Serializable data object Confirmed : TransactionStatus() } + @Serializable sealed class InteractionAddressType { + + @Serializable data class Validator(val address: String) : InteractionAddressType() + + @Serializable data class User(val address: String) : InteractionAddressType() + + @Serializable data class Contract(val address: String) : InteractionAddressType() + + @Serializable data class Multiple(val addresses: List) : InteractionAddressType() } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt similarity index 93% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt index 639de2993b..af3fb7323d 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigDecimalSerializer.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt similarity index 93% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt index 93394b1e0c..f9be82fe0b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/BigIntegerSerializer.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt similarity index 77% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt index 3243892b18..c75fe1644b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigDecimal.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.Serializable import java.math.BigDecimal diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt similarity index 77% rename from domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt index 8fbbda7eea..d5a6847870 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/SerializedBigInteger.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.core.serialization +package com.tangem.domain.models.serialization import kotlinx.serialization.Serializable import java.math.BigInteger diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt index 0f59dfe679..f5aef4acfa 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/staking/YieldBalanceItem.kt @@ -1,6 +1,6 @@ package com.tangem.domain.models.staking -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.staking.action.StakingActionType import kotlinx.datetime.Instant import kotlinx.serialization.Serializable diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt index f83aec887a..0fdd408cc5 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt @@ -1,8 +1,8 @@ package com.tangem.domain.nft.models -import com.tangem.domain.core.serialization.SerializedBigInteger import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network +import com.tangem.domain.models.serialization.SerializedBigInteger import kotlinx.serialization.Serializable @Serializable diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 43bc8a4a29..bac263726a 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -1,6 +1,6 @@ package com.tangem.domain.nft.models -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt index a6ffb9df16..29e09d2151 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt @@ -1,6 +1,6 @@ package com.tangem.domain.onramp.model -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable @Serializable diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index fb76a29bea..352c2d75e2 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -2,10 +2,10 @@ package com.tangem.domain.onramp.model.cache import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampStatus -import com.tangem.domain.models.wallet.UserWalletId /** * Model for local storing onramp transaction diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt index ab5a2807a0..1a2b6103e1 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt @@ -2,9 +2,9 @@ package com.tangem.domain.onramp import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository class GetLegacyTopUpUrlUseCase( private val legacyTopUpRepository: LegacyTopUpRepository, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 75a9e20b8f..093fd13bef 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -1,6 +1,6 @@ package com.tangem.domain.staking.model.stakekit -import com.tangem.domain.core.serialization.SerializedBigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal import com.tangem.domain.models.staking.YieldToken import kotlinx.serialization.Serializable diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index acfaaf4d46..81eb91fe42 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -1,7 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** * Model of currencies available to swap diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt index 5e8e0479f9..863d43017e 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt @@ -1,7 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** * Domain layer representation of SwapPair data network model. diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index d3a0b037c7..6fa54f8771 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -4,12 +4,12 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDataModel import com.tangem.domain.swap.models.SwapPairModel import com.tangem.domain.swap.models.SwapQuoteModel import com.tangem.domain.swap.models.SwapStatusModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 6370564cc2..ac60b660e2 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -5,11 +5,11 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.SwapDataModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus @Suppress("LongParameterList") class GetSwapDataUseCase( diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt index 55fe6a22b4..5dfe8cda5e 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -3,14 +3,14 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet /** * Get list of swap pairs diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index db527b65a1..5f923070f5 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 @@ -10,7 +11,6 @@ import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Returns pais diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt index ed6c8a1d27..dcfd5cffb4 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SelectInitialPairUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.swap.usecase import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapTransactionRepository import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.getGroupWithDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.extensions.orZero /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index e254c348c4..5cebffc055 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -3,6 +3,8 @@ package com.tangem.domain.swap.usecase import arrow.core.Either import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType.Companion.shouldStoreSwapTransaction +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.SwapTransactionRepository @@ -10,8 +12,6 @@ import com.tangem.domain.swap.models.SwapDataTransactionModel import com.tangem.domain.swap.models.SwapStatus import com.tangem.domain.swap.models.SwapStatusModel import com.tangem.domain.swap.models.SwapTransactionModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet @Suppress("LongParameterList") class SwapTransactionSentUseCase( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 0d6b285977..9bba447d40 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -2,13 +2,13 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt index 807e118321..bc8e4fa72d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetBalanceNotEnoughForFeeWarningUseCase.kt @@ -2,11 +2,11 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e1b0f65499..e219fbf94c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.promo.models.StoryContentIds @@ -12,11 +15,8 @@ import com.tangem.domain.tokens.actions.CommonActionsFactory import com.tangem.domain.tokens.actions.MissedDerivationsActionsFactory import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory import com.tangem.domain.tokens.actions.UnreachableActionsFactory -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt index 329794c3ae..43c65d397a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -1,10 +1,10 @@ package com.tangem.domain.tokens import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrencyChecksRepository -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index eb1f7d4bd6..6b64410367 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -3,8 +3,9 @@ package com.tangem.domain.tokens import com.tangem.blockchainsdk.utils.isNeedToCreateAccountWithoutReserve import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -15,7 +16,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt index 640a3f0726..77bb61fb5e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt @@ -2,12 +2,12 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId class GetFeePaidCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt index 72ee9c9df4..71f50c03d7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt @@ -2,9 +2,9 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.repository.CurrencyChecksRepository import java.math.BigDecimal class GetMinimumTransactionAmountSyncUseCase( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt index 34faf60620..63b74c9631 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMultiCryptoCurrencyStatusUseCase.kt @@ -1,12 +1,12 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow class GetMultiCryptoCurrencyStatusUseCase( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 5179b54d12..89a9baade6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -2,15 +2,15 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt index 1bd4c4f678..369303c3cb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetSingleCryptoCurrencyStatusUseCase.kt @@ -2,12 +2,12 @@ package com.tangem.domain.tokens import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index fde614c26c..a7b6edcee0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -4,14 +4,14 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.map diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index c3a08ac9a3..1d02262cc7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -9,9 +9,9 @@ import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 6feda808cf..5863373ebf 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -2,11 +2,11 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.transaction.models.AssetRequirementsCondition diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index 41890a7320..522690bc10 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -2,13 +2,13 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index f38522a4f7..adf28072be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -2,12 +2,12 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index ded293f444..c41c4826e4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -1,11 +1,11 @@ package com.tangem.domain.tokens.actions import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState.ActionState import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index b7f00f81cb..1cf1651c9d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.legacy -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import org.rekotlin.Action sealed class TradeCryptoAction : Action { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt index 19aae81b6d..f771dd1795 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index d822bddd38..306a8cba00 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -1,7 +1,8 @@ package com.tangem.domain.tokens.model -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.staking.model.stakekit.Yield data class TokenActionsState( val walletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 1ade11835e..5cd12a6b8b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.model import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import java.math.BigDecimal /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt index 11ad1a755f..d3842665be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrenciesStatusesOperations.kt @@ -1,9 +1,9 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus /** * Base operations for working with currencies statuses diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index abf478099e..e39261d0a7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -5,9 +5,12 @@ import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -17,9 +20,7 @@ import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.isStakingSupported -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer @@ -27,7 +28,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 9d0dfe2461..fcce30d9fd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -11,9 +11,12 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -25,9 +28,7 @@ import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer @@ -35,7 +36,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index da8de5018f..c1dc1ffb33 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -2,10 +2,10 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal internal class CurrencyStatusOperations( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 2bd4ff898a..198890fd63 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -3,10 +3,10 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 0d853bbf2c..d2c9b899b3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -6,8 +6,8 @@ import arrow.core.raise.either import arrow.core.raise.withError import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index bec82f8e2a..e6288914b2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -9,10 +9,10 @@ import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.utils.extensions.orZero diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 28522fdc9d..6dd77dfdc2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -3,11 +3,11 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index a971f7d8be..8351958b10 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -1,12 +1,12 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning -import com.tangem.domain.models.wallet.UserWalletId import java.math.BigDecimal interface CurrencyChecksRepository { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index 08138e4e05..bad702ce42 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -5,12 +5,12 @@ import arrow.core.NonEmptyList import arrow.core.raise.either import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index a380587679..1d660f1061 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -5,10 +5,10 @@ import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import java.math.BigDecimal diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index fddd277f37..555289ed3d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,11 +1,11 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.quote.fold -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index b408c3975e..8a0f5fa136 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -5,11 +5,11 @@ import arrow.core.getOrElse import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.first diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt index 253d74b0ce..ac34715c4d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt @@ -3,10 +3,10 @@ package com.tangem.features.markets.portfolio.impl.loader import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState /** diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index caa873be75..5c5bb59b96 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -3,7 +3,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.ArtworkModel -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt index 8601ed1193..e1acbcf4e1 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -4,10 +4,10 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt index 85478242c5..9e5aba6a0a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt @@ -2,7 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 01f73e63a2..4f0a65a810 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -13,19 +13,19 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt index c1cd153231..704923459b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -1,8 +1,8 @@ package com.tangem.features.onboarding.v2.note.impl.model import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet internal data class OnboardingNoteCommonState( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index fa3a235361..2e3a94788a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -30,20 +30,20 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt index a416668d6f..0cdec24a2b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/HotCryptoComponent.kt @@ -2,7 +2,7 @@ package com.tangem.features.onramp.hottokens import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 73b0f0a25a..d889c54e08 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -17,12 +17,12 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selecttoken.OnrampOperationComponent.Params diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt index 38a7a486de..35f44bbb33 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import kotlinx.coroutines.flow.StateFlow /** Token list component that present list of available tokens for swap */ diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt index 056bbaf84f..55e336791d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt @@ -3,7 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index b1a335134f..ccf527232b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.onramp.swap.availablepairs.entity.transformers -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt index 7e9c537c9e..d46f06637e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 12a9b31e16..0fe2bad477 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -16,8 +16,8 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index 52a717c63e..5472e0e61c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.entity.SwapSelectTokensController import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index e7daefe8c7..94ba06e247 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onramp.tokenlist.entity.OnrampOperation diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 2deda8e910..70d687f450 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index b54f7d0386..f693f02e79 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus /** [REDACTED_AUTHOR] diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index c9e0a59aee..8e0f99d7fd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -13,12 +13,12 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt index 1cef61c576..90823b9c00 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendNotificationsComponent.kt @@ -6,9 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.StateFlow import java.math.BigDecimal diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 94c63eef08..785002a836 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -4,7 +4,7 @@ import arrow.core.Either import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt index 181267b68a..3c251c609b 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.utils.extensions.isZero diff --git a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt index 59ea19d75c..76958ee240 100644 --- a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt +++ b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils import com.google.common.truth.Truth.assertThat -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import io.mockk.mockk import org.junit.jupiter.api.Test import java.math.BigDecimal diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt index 864d53807d..139d171f9a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt index 7a2265d453..258312e8a0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomFieldConverter.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt index d1c3b9c9e0..9f826d2181 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.feeselector.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt index b2b45f8a90..684d7f6611 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index c0d9b98bd2..29b3573aa4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -20,7 +20,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index e98b5f4d20..114939b743 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -11,9 +11,9 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 333bfe1c2a..d23c93d631 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.requireColdWallet @@ -32,7 +33,6 @@ import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index fb7ed7c2b5..cc0fe9a936 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -11,20 +11,20 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.nft.models.NFTAsset -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent -import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 873a2c545e..287366db87 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -20,10 +20,10 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index d212be79aa..925d3780df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -3,11 +3,11 @@ package com.tangem.features.send.v2.subcomponents.amount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback import kotlinx.coroutines.flow.StateFlow diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 58c464e03a..28b1e1cc38 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -22,10 +22,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendFeatureToggles diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt index bdf1038ebe..dcd8c7ca34 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt @@ -3,9 +3,9 @@ package com.tangem.features.send.v2.subcomponents.fee import arrow.core.Either import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import kotlinx.coroutines.flow.Flow diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt index 939bb73a64..11d3e108d9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.extensions.isZero diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt index d66b9376e2..7891a1fd2c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -3,9 +3,9 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 820e44dbb4..344269f818 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -4,12 +4,12 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index efb7fee254..821aac24fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index b16dd562a8..85cea0393c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -9,10 +9,10 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 6a380e8e4c..c72ed83f63 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -10,7 +10,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT @@ -18,7 +19,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index 759cf62c33..de65e73033 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -10,7 +10,8 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT @@ -18,7 +19,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index c3dd23ba75..66607e3750 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt index 65d07205da..495efa22af 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents -import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter import com.tangem.utils.transformer.Transformer internal class SendFeeCustomAutoFixTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt index 5d95767e59..1569cd7d3b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt @@ -1,11 +1,11 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.utils.transformer.Transformer internal class SendFeeCustomValueChangeTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt index 2a2b88a7d2..498ced2b6b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.lib.crypto.BlockchainUtils.isTron diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt index 89a69d49f0..3c16c06146 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt index 6ae4b1b0d3..853aff08fe 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.send.v2.subcomponents.fee.model.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter -import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.utils.transformer.Transformer internal class SendFeeSelectTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 8f2dabbed3..da00a9307b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -25,12 +25,12 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 65ffcfdedb..131d64fb11 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -5,11 +5,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.models.staking.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* internal class StakingAnalyticSender( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 90a4867e0c..d5efc4972b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -29,11 +29,9 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.staking.BalanceItem -import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.models.staking.PendingAction -import com.tangem.domain.models.staking.RewardBlockType -import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.* +import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -42,14 +40,13 @@ import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.models.staking.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 9476521ace..15cc4b24ae 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -6,14 +6,14 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.BalanceType.Companion.isClickable -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.lib.crypto.BlockchainUtils diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index af1a4bde08..3643ba7e27 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -6,11 +6,11 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 0ed2a3814c..4251a689ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -5,14 +5,14 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.utils.getRewardStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.YieldReward import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 0cd6d2468c..3defda6f74 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -1,14 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.helpers +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.FetchActionsUseCase import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.coroutines.DelayedWork import dagger.assisted.Assisted diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index 94e68d2aad..c54feee187 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -8,20 +8,20 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.staking.EstimateGasUseCase +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.EstimateGasUseCase import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index f2a1574d06..8223f07a1d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -5,13 +5,15 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.NetworkType +import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.GetConstructedStakingTransactionUseCase import com.tangem.domain.staking.GetStakingTransactionsUseCase import com.tangem.domain.staking.SaveUnsubmittedHashUseCase import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.SubmitHashData -import com.tangem.domain.models.staking.NetworkType -import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -19,14 +21,12 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.staking.getCurrentToken import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index d4ca71cd71..936fea5c1b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -7,12 +7,12 @@ import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIco import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.model.StakingClickIntents +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index c9a8054cc5..73b8c296f2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 7190ad5ae1..7eb0589c34 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index 99b2bd7041..7fb48b2a61 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -1,12 +1,12 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.StakingApproval +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.PendingAction +import com.tangem.domain.models.staking.action.StakingActionType +import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.models.staking.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.features.staking.impl.presentation.state.utils.isTronStakedBalance diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index 327ec4ae8e..db0da4876d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e2e24ff112..9a319caa8a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -14,10 +14,10 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceItem -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index e905b0b0a5..0603ec9e7f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt index 0410bf8745..9631543416 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 318d0c6522..cee87196ee 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt index f5e92f66ec..ce81a3c55c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt index 5f7b5e5e57..026ab044df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 321db52473..f24052ac95 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -11,10 +11,10 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.extensions.isPositive diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt index d2b2e7fe50..26deb1759b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRoundToIntegerTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import java.math.RoundingMode diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 0729792529..235472ef08 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.approva import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index be9aac4f36..c70f2da7a7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 122420439f..0dbf76f8ad 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -14,11 +14,11 @@ import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.StakingErrors import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index ee2c810e32..8959e90124 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -4,12 +4,12 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.BalanceType -import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.models.staking.YieldBalance -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.models.staking.action.StakingActionType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingNotification diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt index b4d42058e1..3f99aba73d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.utils -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.lib.crypto.BlockchainUtils.isTron import java.math.BigDecimal import java.math.MathContext diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index c5e112b2f4..109c84dd81 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -3,9 +3,9 @@ package com.tangem.features.swap.v2.impl.amount import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 26267a4710..fea9701d97 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -6,9 +6,9 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 23c478c056..f1d6be3848 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -17,6 +17,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.swap.models.SwapCurrencies @@ -27,7 +28,6 @@ import com.tangem.domain.swap.models.getGroupWithDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index c4869a1825..06fb3dc71b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 8745ee371d..f29f727166 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -10,9 +10,9 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index 2b25b3314b..9aeeb9f2d7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -3,10 +3,10 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index c4ba35f561..9f54cc94b6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -3,10 +3,10 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt index dde5dd9c4c..5ed86c33d1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.utils.transformer.Transformer diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index d7fd561378..e7bf485c35 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -9,10 +9,10 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index d902dee096..995f10c9ae 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -2,7 +2,7 @@ package com.tangem.features.swap.v2.impl.common import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import java.math.BigDecimal diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 1d02dcce0c..47b374269f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -10,9 +10,9 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDirection -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.entity.PredefinedValues diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index d4a416b177..847f6d100b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -16,10 +16,10 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index ace6ba9272..f6c595b273 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.express.models.ExpressError import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDataModel import com.tangem.domain.swap.models.SwapDataTransactionModel import com.tangem.domain.swap.usecase.GetSwapDataUseCase import com.tangem.domain.swap.usecase.SwapTransactionSentUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index d8a7e41bde..e370965fe4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -13,13 +13,13 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index abbcf27ea2..bf97fe58d9 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.domain.models.domain import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import java.math.BigDecimal /** diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index be89782000..e8e10f7165 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt index 85fc60140b..b71ed02569 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt index 3c27003179..c62a113adc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 673cc0e7fc..3646a0d6b5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f0fbc9fbcc..cc8f64ca53 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -18,12 +18,14 @@ import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -31,8 +33,6 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 450234b855..d31fdcffe5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency import com.tangem.feature.swap.models.SwapSelectTokenStateHolder diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index d0b4f81377..a748df6f52 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -29,7 +29,11 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -40,11 +44,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 69f4309c55..36018d5189 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index 86a8a33195..604203a8f7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.model -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a041840318..2364dbd0a6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -10,7 +10,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index ad13b81ea0..9f250ee99b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -18,8 +18,8 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.promo.models.StoryContent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt index 9a17d9be3a..04b8498da4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsCurrencyStatusAnalyticsSender.kt @@ -4,8 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus internal class TokenDetailsCurrencyStatusAnalyticsSender( private val analyticsEventHandler: AnalyticsEventHandler, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 33f4a326c7..f5352430b4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -36,6 +36,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -49,7 +50,6 @@ import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability 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.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index c2f97a9c09..95139be280 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -4,9 +4,9 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.utils.Provider diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index da60bfba2b..03fbaa9901 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -9,10 +9,10 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index a18b9be6e1..ec37185065 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -16,9 +16,9 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 2a7dfc2ef6..f6fb474788 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -8,13 +8,13 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.staking.utils.getTotalStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 251bfb5dfd..22365b4892 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -15,6 +15,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.TxInfo @@ -23,7 +24,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 5b3760c7f4..46a530ddbe 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -6,11 +6,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index c646572718..2e91b8b73d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -6,15 +6,15 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus.Status.* -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index 1cf9437129..b5586fdb53 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -8,14 +8,14 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionStateConverter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 06cea48302..cc5a440140 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -4,17 +4,17 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.TokensAction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index b58f51c8ef..29c08244a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -30,8 +30,11 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -41,15 +44,12 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase 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.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 037bed6176..21f42375ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -7,9 +7,9 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.YieldBalance import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 846fe77ccb..8c39cfc9e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -6,12 +6,12 @@ import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.redux.StateDialog -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import kotlinx.coroutines.channels.BufferOverflow diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index c33e3a6652..b1670c0110 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,8 +2,8 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 1ce39be795..9c7d278dd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -13,10 +13,10 @@ import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 2e1f9c25f6..24cd9574d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -8,12 +8,12 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index ab95132fed..64d80d9754 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -6,15 +6,15 @@ import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt index 3e917b7417..58da9fca4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import kotlinx.coroutines.flow.* import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt index 586234c6d6..9f31104f3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.extensions.isZero +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import javax.inject.Inject diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index 822e377075..b3ebdd5713 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -1,16 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import kotlinx.collections.immutable.toPersistentList import timber.log.Timber diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index 4c6f769a5e..4e5b37f5d9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 1201f8807a..2accc6f5b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -9,16 +9,12 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.common.util.getCardsCount -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.exception.RefreshTokenExpiredException import com.tangem.domain.visa.model.VisaCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.utils.extensions.isZero import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 98c83885fa..97a13ed361 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -3,13 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig -import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 4df4316649..fe8a06fbc8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 3611aa8119..8dadf5a69e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -8,7 +8,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index 5b487cb700..73813e73d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -15,9 +15,9 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 9a3c8407b4..83cdcb49cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -7,8 +7,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -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.child.wallet.model.intents.WalletClickIntents diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 1fa58e9e7d..4532a34ec2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -7,11 +7,11 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index 7600075e6a..a2d9cbdd89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -5,13 +5,13 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index df61988921..e5deefbf1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -5,14 +5,14 @@ import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 463c89fc5a..fc2dac78e4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -17,11 +17,11 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletconnect.WcAnalyticEvents From 4c51193221e547bf3446ef20efef4eb49f247559 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 17:42:54 +0400 Subject: [PATCH 020/165] Updated on 2026-08-14 --- .../com/tangem/domain/models/StatusSource.kt | 3 + .../tangem/domain/models/TotalFiatBalance.kt | 9 +- .../domain/models/tokenlist/TokenList.kt | 88 +++++++++++++++++++ .../domain/tokens/GetTokenListUseCase.kt | 2 +- .../tokens/ToggleTokenListGroupingUseCase.kt | 2 +- .../tokens/ToggleTokenListSortingUseCase.kt | 2 +- .../domain/tokens/error/TokenListError.kt | 2 +- .../domain/tokens/model/NetworkGroup.kt | 17 ---- .../tangem/domain/tokens/model/TokenList.kt | 66 -------------- .../tokens/operations/TokenListOperations.kt | 2 +- .../operations/TokenListSortingOperations.kt | 4 +- .../domain/tokens/mock/MockNetworksGroups.kt | 2 +- .../domain/tokens/mock/MockTokenLists.kt | 2 +- .../model/AvailableSwapPairsModel.kt | 4 +- .../tokenlist/model/OnrampTokenListModel.kt | 2 +- .../model/OrganizeTokensModel.kt | 2 +- .../OrganizeTokensStateHolder.kt | 2 +- .../utils/CryptoCurrenciesIdsResolver.kt | 2 +- .../utils/common/TokenListOperations.kt | 2 +- .../converter/TokenListToStateConverter.kt | 2 +- .../NetworkGroupToDraggableItemsConverter.kt | 2 +- .../items/TokenListToListStateConverter.kt | 2 +- .../utils/TokenListAnalyticsSender.kt | 2 +- .../domain/GetMultiWalletWarningsFactory.kt | 2 +- .../domain/MultiWalletTokenListStore.kt | 4 +- .../wallet/domain/WalletWithFundsChecker.kt | 2 +- .../transformers/SetTokenListTransformer.kt | 4 +- .../converter/TokenListStateConverter.kt | 4 +- .../subscribers/BasicTokenListSubscriber.kt | 2 +- .../MultiWalletTokenListSubscriber.kt | 2 +- .../SingleWalletWithTokenListSubscriber.kt | 4 +- 31 files changed, 130 insertions(+), 117 deletions(-) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt index 73edfce845..641ec0eaa7 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt @@ -1,10 +1,13 @@ package com.tangem.domain.models +import kotlinx.serialization.Serializable + /** * Source of the status of any loaded data * [REDACTED_AUTHOR] */ +@Serializable enum class StatusSource { /** diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt index e7b9ccdc03..374956a548 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TotalFiatBalance.kt @@ -1,22 +1,26 @@ package com.tangem.domain.models -import java.math.BigDecimal +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable /** * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount */ +@Serializable sealed interface TotalFiatBalance { /** * Represents the loading state of the fiat balance. * This state indicates that the fiat balance is currently being retrieved or calculated. */ + @Serializable data object Loading : TotalFiatBalance /** * Represents the failure state of the fiat balance. * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. */ + @Serializable data object Failed : TotalFiatBalance /** @@ -25,8 +29,9 @@ sealed interface TotalFiatBalance { * @property amount the loaded fiat balance amount * @property isAllAmountsSummarized indicates whether the amount includes a summary of all underlying amounts */ + @Serializable data class Loaded( - val amount: BigDecimal, + val amount: SerializedBigDecimal, val isAllAmountsSummarized: Boolean, val source: StatusSource, ) : TotalFiatBalance diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt new file mode 100644 index 0000000000..bec343c4df --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/tokenlist/TokenList.kt @@ -0,0 +1,88 @@ +package com.tangem.domain.models.tokenlist + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +/** + * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. + * + * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. + * Additional details like the total fiat balance and the sorting type can be associated with the list. + */ +@Serializable +sealed interface TokenList { + + /** The total fiat balance across all tokens */ + val totalFiatBalance: TotalFiatBalance + + /** The criteria used for sorting the tokens */ + val sortedBy: TokensSortType + + /** + * Represents tokens that are grouped by their network + * + * @property totalFiatBalance the total fiat balance across all groups + * @property sortedBy the criteria used for sorting the tokens within the groups + * @property groups a list of network groups containing tokens + */ + @Serializable + data class GroupedByNetwork( + override val totalFiatBalance: TotalFiatBalance, + override val sortedBy: TokensSortType, + val groups: List, + ) : TokenList { + + /** + * Represents a group of cryptocurrencies associated with a specific network + * + * @property network the blockchain network associated with the group + * @property currencies a list of cryptocurrency statuses that belong to the network + */ + @Serializable + data class NetworkGroup( + val network: Network, + val currencies: List, + ) + } + + /** + * Represents tokens that are not grouped by any specific criteria. + * + * @property totalFiatBalance the total fiat balance across all groups + * @property sortedBy the criteria used for sorting the tokens within the groups + * @property currencies a list of cryptocurrency statuses + */ + @Serializable + data class Ungrouped( + override val totalFiatBalance: TotalFiatBalance, + override val sortedBy: TokensSortType, + val currencies: List, + ) : TokenList + + /** Represents a state where the token list is empty */ + @Serializable + data object Empty : TokenList { + + override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( + amount = SerializedBigDecimal.ZERO, + isAllAmountsSummarized = true, + source = StatusSource.ACTUAL, + ) + + override val sortedBy: TokensSortType = TokensSortType.NONE + } + + /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ + fun flattenCurrencies(): List { + return when (this) { + is GroupedByNetwork -> groups.flatMap(GroupedByNetwork.NetworkGroup::currencies) + is Ungrouped -> currencies + is Empty -> emptyList() + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index a7b6edcee0..c1d7cd8cc8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -5,10 +5,10 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt index 936723bc4d..b0b9ac0e7c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -6,9 +6,9 @@ import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.raise.withError import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt index e282aed62d..9283f50615 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -7,9 +7,9 @@ import arrow.core.raise.ensure import arrow.core.raise.withError import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.operations.TokenListSortingOperations import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt index 5332319220..e1afb549a0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.error -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList sealed class TokenListError { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt deleted file mode 100644 index f771dd1795..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network - -/** - * Represents a group of cryptocurrencies associated with a specific network. - * - * This class encapsulates a collection of cryptocurrency statuses, all of which are part of the same blockchain network. - * - * @property network The blockchain network associated with the group. - * @property currencies A list of cryptocurrency statuses that belong to the network. - */ -data class NetworkGroup( - val network: Network, - val currencies: List, -) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt deleted file mode 100644 index 5cd12a6b8b..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import java.math.BigDecimal - -/** - * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. - * - * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. - * Additional details like the total fiat balance and the sorting type can be associated with the list. - * - * @property totalFiatBalance The total fiat balance across all tokens, which could be in a loading state, failed, or loaded with a specific amount. - * @property sortedBy The criteria used for sorting the tokens. - */ -sealed class TokenList { - open val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loading - open val sortedBy: TokensSortType = TokensSortType.NONE - - /** - * Represents tokens that are grouped by their network. - * - * @property groups A list of network groups containing tokens. - * @property totalFiatBalance The total fiat balance across all groups. - * @property sortedBy The criteria used for sorting the tokens within the groups. - */ - data class GroupedByNetwork( - val groups: List, - override val totalFiatBalance: TotalFiatBalance, - override val sortedBy: TokensSortType, - ) : TokenList() - - /** - * Represents tokens that are not grouped by any specific criteria. - * - * @property currencies A list of cryptocurrency statuses. - * @property totalFiatBalance The total fiat balance across all currencies. - * @property sortedBy The criteria used for sorting the currencies. - */ - data class Ungrouped( - val currencies: List, - override val totalFiatBalance: TotalFiatBalance, - override val sortedBy: TokensSortType, - ) : TokenList() - - /** Represents a state where the token list is empty. */ - data object Empty : TokenList() { - - override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( - amount = BigDecimal.ZERO, - isAllAmountsSummarized = true, - source = StatusSource.ACTUAL, - ) - } - - /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ - fun flattenCurrencies(): List { - return when (this) { - is GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies) - is Ungrouped -> currencies - is Empty -> emptyList() - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index d2c9b899b3..01eac8c1bb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -7,8 +7,8 @@ import arrow.core.raise.withError import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.flow.* diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index e6288914b2..7669211249 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -12,9 +12,9 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.staking.YieldBalance +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList import com.tangem.utils.extensions.orZero import java.math.BigDecimal diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt index d69fa589b0..6cc7313c8f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup @Suppress("MemberVisibilityCanBePrivate") internal object MockNetworksGroups { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 1d660f1061..caa2c13c6f 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -6,10 +6,10 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups -import com.tangem.domain.tokens.model.TokenList import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 0fe2bad477..18f0ca746d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -17,8 +17,8 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo @@ -200,7 +200,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .collectLatest { selectedStatus -> val networkInfo = selectedStatus.toLeastTokenInfo() - val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() ?: false + val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 8e0f99d7fd..d2ce88c2d0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -14,13 +14,13 @@ import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index 7aeebcd81e..4f7dfafd03 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -12,12 +12,12 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase import com.tangem.domain.tokens.ToggleTokenListSortingUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index 8b112a4dd6..193ed15375 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index fc72339e53..74bec371ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt index 4ff807e1f8..8600d8beaf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.domain.models.TokensSortType -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList internal fun TokenList.disableSortingByBalance(): TokenList { return when (this) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt index 12a713c0ff..d561e3b262 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter import com.tangem.domain.models.TokensSortType -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt index 0205df476a..73ecfe3f79 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup 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.getGroupPlaceholder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt index 455e896da9..770d63c0f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList 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.uniteItems diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 9c7d278dd0..bf5b27d5ea 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -14,10 +14,10 @@ import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 24cd9574d3..4427d9de7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -9,12 +9,12 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt index 167deff237..1aa129675c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.SharingStarted diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt index 9f31104f3a..6561164dd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -2,8 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.extensions.isZero import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.model.TokenList import javax.inject.Inject internal class WalletWithFundsChecker @Inject constructor( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 54ff2afed0..e8b3b2446f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,15 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import timber.log.Timber internal class SetTokenListTransformer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 83cdcb49cb..ec332f336e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -8,9 +8,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index e98689e34e..08c415f5b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -6,10 +6,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index e9dbc7eb6f..b719923f57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -6,11 +6,11 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index e93d8962f6..80027a5ec1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore From c8a181c829601ac4b4e4b60e910508ee45fd4496 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Jul 2025 12:29:05 +0500 Subject: [PATCH 021/165] Updated on 2026-08-14 --- .../swap/DefaultSwapTransactionRepository.kt | 57 +++++++++---------- .../transaction/SavedSwapStatusConverter.kt | 6 +- .../SavedSwapTransactionConverter.kt | 17 +++++- .../swap/models/SwapTransactionListDTO.kt | 5 +- .../tangem/data/swap/models/SwapTxTypeDTO.kt | 13 +++++ .../swap/models/SwapTransactionListModel.kt | 1 + .../tangem/domain/swap/models/SwapTxType.kt | 6 ++ .../domain/swap/SwapTransactionRepository.kt | 6 +- .../usecase/SwapTransactionSentUseCase.kt | 24 ++++---- .../confirm/model/SwapTransactionSender.kt | 2 + .../swap/DefaultSwapTransactionRepository.kt | 32 ++++++++--- .../SavedSwapTransactionListConverter.kt | 25 ++++---- .../domain/SavedSwapTransactionListModel.kt | 14 ++++- 13 files changed, 135 insertions(+), 73 deletions(-) create mode 100644 data/swap/src/main/java/com/tangem/data/swap/models/SwapTxTypeDTO.kt create mode 100644 domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTxType.kt diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index c2d3527060..072cf92212 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -13,7 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync -import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -24,9 +24,8 @@ import com.tangem.domain.swap.models.SwapTransactionModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -95,36 +94,34 @@ internal class DefaultSwapTransactionRepository( } } - override suspend fun getTransactions( + override fun getTransactions( userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, - ): Flow?> { - return withContext(dispatchers.io) { - val txStatuses = appPreferencesStore.getObjectMapSync( - key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, - ) - appPreferencesStore.getObjectList( - key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, - ).map { savedTransactions -> - val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWallet.walletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) - } + ): Flow?> = combine( + flow = appPreferencesStore.getObjectList( + key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, + ), + flow2 = appPreferencesStore.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ), + ) { savedTransactions, txStatuses -> + val currencyTxs = savedTransactions + ?.filter { + it.userWalletId == userWallet.walletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } - currencyTxs?.mapNotNull { - listConverter.convertBack( - value = it, - userWallet = userWallet, - txStatuses = txStatuses, - ) - } - }.flowOn(dispatchers.io) + currencyTxs?.mapNotNull { + listConverter.convertBack( + value = it, + userWallet = userWallet, + txStatuses = txStatuses, + ) } - } + }.flowOn(dispatchers.default) override suspend fun removeTransaction( userWalletId: UserWalletId, @@ -188,7 +185,7 @@ internal class DefaultSwapTransactionRepository( ) val updatesMap = savedMap.toMutableMap() - updatesMap[txId] = savedStatusConverter.convertBack( + updatesMap[txId] = savedStatusConverter.convert( status.copy( refundTokensResponse = refundTokenCurrency?.let { userTokensResponseFactory.createResponseToken(refundTokenCurrency) diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt index c3d8f36903..20c6016104 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapStatusConverter.kt @@ -6,9 +6,9 @@ import com.tangem.domain.swap.models.SwapStatus import com.tangem.domain.swap.models.SwapStatusModel import com.tangem.utils.converter.TwoWayConverter -internal class SavedSwapStatusConverter : TwoWayConverter { +internal class SavedSwapStatusConverter : TwoWayConverter { - override fun convert(value: SwapStatusDTO) = SwapStatusModel( + override fun convertBack(value: SwapStatusDTO) = SwapStatusModel( providerId = value.providerId, status = SwapStatus.entries.firstOrNull { it.name.lowercase() == value.status?.name?.lowercase() @@ -22,7 +22,7 @@ internal class SavedSwapStatusConverter : TwoWayConverter?> diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index 5cebffc055..8d6869366a 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -8,10 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.SwapTransactionRepository -import com.tangem.domain.swap.models.SwapDataTransactionModel -import com.tangem.domain.swap.models.SwapStatus -import com.tangem.domain.swap.models.SwapStatusModel -import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.domain.swap.models.* @Suppress("LongParameterList") class SwapTransactionSentUseCase( @@ -28,15 +25,8 @@ class SwapTransactionSentUseCase( provider: ExpressProvider, txHash: String, timestamp: Long, + swapTxType: SwapTxType, ) = Either.catch { - swapRepositoryV2.swapTransactionSent( - userWallet = userWallet, - fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, - toAddress = swapDataTransactionModel.txTo, - txId = swapDataTransactionModel.txId, - txHash = txHash, - txExtraId = swapDataTransactionModel.txExtraId, - ) if (provider.type.shouldStoreSwapTransaction()) { swapTransactionRepository.storeTransaction( userWalletId = userWallet.walletId, @@ -56,12 +46,22 @@ class SwapTransactionSentUseCase( txExternalId = (swapDataTransactionModel as? SwapDataTransactionModel.CEX)?.externalTxId, averageDuration = null, ), + swapTxType = swapTxType, ), ) } + swapTransactionRepository.storeLastSwappedCryptoCurrencyId( userWalletId = userWallet.walletId, cryptoCurrencyId = toCryptoCurrencyStatus.currency.id, ) + swapRepositoryV2.swapTransactionSent( + userWallet = userWallet, + fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, + toAddress = swapDataTransactionModel.txTo, + txId = swapDataTransactionModel.txId, + txHash = txHash, + txExtraId = swapDataTransactionModel.txExtraId, + ) }.mapLeft(swapErrorResolver::resolve) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index f6c595b273..458d1ebd4e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDataModel import com.tangem.domain.swap.models.SwapDataTransactionModel +import com.tangem.domain.swap.models.SwapTxType import com.tangem.domain.swap.usecase.GetSwapDataUseCase import com.tangem.domain.swap.usecase.SwapTransactionSentUseCase import com.tangem.domain.transaction.error.SendTransactionError @@ -158,6 +159,7 @@ internal class SwapTransactionSender @AssistedInject constructor( provider = provider, txHash = txHash, timestamp = timestamp, + swapTxType = SwapTxType.SendWithSwap, ) onSendSuccess(txHash, timestamp, swapData) }, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index e5c25f522c..d4fec5e8ed 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -92,21 +92,37 @@ internal class DefaultSwapTransactionRepository( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), ) { savedTransactions, txStatuses -> - val currencyTxs = savedTransactions?.filter { - it.userWalletId == userWallet.walletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) + + val currencyToTxs = savedTransactions?.filter { + val isUserWallet = it.userWalletId == userWallet.walletId.stringValue + val toCurrency = it.toCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && toCurrency } - currencyTxs?.mapNotNull { + val currencyFromTxs = savedTransactions?.filter { + val isUserWallet = it.userWalletId == userWallet.walletId.stringValue + val fromCurrency = it.fromCryptoCurrencyId == cryptoCurrencyId.value + isUserWallet && fromCurrency + } + + val toTxs = currencyToTxs?.mapNotNull { + converter.convertBack( + value = it, + userWallet = userWallet, + txStatuses = txStatuses, + onFilter = { it.swapTxTypeDTO == SwapTxTypeDTO.Swap }, + ) + }.orEmpty() + + val fromTxs = currencyFromTxs?.mapNotNull { converter.convertBack( value = it, userWallet = userWallet, txStatuses = txStatuses, ) - } + }.orEmpty() + + fromTxs + toTxs } .flowOn(dispatchers.default) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 4735f63fdc..abef06ede4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -34,6 +34,7 @@ internal class SavedSwapTransactionListConverter( value: SavedSwapTransactionListModelInner, userWallet: UserWallet, txStatuses: Map, + onFilter: (SavedSwapTransactionModel) -> Boolean = { true }, ): SavedSwapTransactionListModel? { val fromToken = value.fromTokensResponse val toToken = value.toTokensResponse @@ -50,17 +51,19 @@ internal class SavedSwapTransactionListConverter( ) ?: return null return SavedSwapTransactionListModel( - transactions = value.transactions.map { tx -> - val status = txStatuses[tx.txId] - val refundCurrency = status?.refundTokensResponse?.let { id -> - responseCryptoCurrenciesFactory.createCurrency( - responseToken = id, - userWallet = userWallet, - ) - } - val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) - tx.copy(status = statusWithRefundCurrency) - }, + transactions = value.transactions + .filter(onFilter) + .map { tx -> + val status = txStatuses[tx.txId] + val refundCurrency = status?.refundTokensResponse?.let { id -> + responseCryptoCurrenciesFactory.createCurrency( + responseToken = id, + userWallet = userWallet, + ) + } + val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) + tx.copy(status = statusWithRefundCurrency) + }, userWalletId = value.userWalletId, fromCryptoCurrencyId = value.fromCryptoCurrencyId, toCryptoCurrencyId = value.toCryptoCurrencyId, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index 68fea21eff..94742cbac9 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -50,4 +50,16 @@ data class SavedSwapTransactionModel( val provider: SwapProvider, @Json(name = "status") val status: ExchangeStatusModel? = null, -) \ No newline at end of file + @Json(name = "swapTxType") + val swapTxTypeDTO: SwapTxTypeDTO? = SwapTxTypeDTO.Swap, +) + +// TODO refactor to use separate models to store +@JsonClass(generateAdapter = false) +enum class SwapTxTypeDTO { + @Json(name = "Swap") + Swap, + + @Json(name = "SendWithSwap") + SendWithSwap, +} \ No newline at end of file From 50c1623bfa6da0fee35f0eaeb702fddcb414ffb2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Jul 2025 14:54:44 +0500 Subject: [PATCH 022/165] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/appbar/TangemTopAppBar.kt | 4 ++-- .../tangem/core/ui/components/notifications/Notification.kt | 4 ++-- .../tangem/features/onramp/main/ui/OnrampProviderContent.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt index c963651693..c936234a4b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt @@ -129,7 +129,7 @@ fun TangemTopAppBar( TopAppBarButton( button = endButton, tint = iconTint, - modifier = modifier.testTag(TopAppBarTestTags.MORE_BUTTON), + modifier = Modifier.testTag(TopAppBarTestTags.MORE_BUTTON), ) } }, @@ -175,7 +175,7 @@ fun TangemTopAppBar( TopAppBarButton( button = startButton, tint = iconTint, - modifier = modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON), + modifier = Modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 1e8c2f342d..1e2279b8c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -208,7 +208,7 @@ internal fun TextsBlock( text = titleText, color = titleColor, style = TangemTheme.typography.button, - modifier = modifier.testTag(NotificationTestTags.TITLE), + modifier = Modifier.testTag(NotificationTestTags.TITLE), ) SpacerH(height = TangemTheme.dimens.spacing2) @@ -218,7 +218,7 @@ internal fun TextsBlock( text = subtitle.resolveReference(), color = subtitleColor, style = TangemTheme.typography.caption2, - modifier = modifier.testTag(NotificationTestTags.TEXT), + modifier = Modifier.testTag(NotificationTestTags.TEXT), ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index 8d2bf768e6..3693715b68 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -62,7 +62,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE), + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE), ) Text( text = buildAnnotatedString { @@ -72,7 +72,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: }, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, - modifier = modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT), + modifier = Modifier.testTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT), ) } AnimatedVisibility( From 02f6e52f46248028c662f51f12a5da92c56424b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 09:53:17 +0500 Subject: [PATCH 023/165] Updated on 2026-08-14 --- .../entry/AddExistingWalletModel.kt | 15 +- .../routing/AddExistingWalletChildFactory.kt | 3 + .../entry/routing/AddExistingWalletRoute.kt | 7 +- .../port/AddExistingWalletImportComponent.kt | 3 +- .../model/AddExistingWalletImportModel.kt | 30 +++- .../port/ui/AddExistingWalletImportContent.kt | 5 +- .../ManualBackupCompletedComponent.kt | 4 +- .../completed/ManualBackupCompletedModel.kt | 2 +- .../setaccesscode/AccessCodeComponent.kt | 26 +--- .../setaccesscode/AccessCodeModel.kt | 51 ++++++- .../setaccesscode/entity/AccessCodeUM.kt | 2 + .../hotwallet/setaccesscode/ui/AccessCode.kt | 136 ++++++++++++++++++ .../setaccesscode/ui/AccessCodeEnter.kt | 106 -------------- .../setaccesscode/ui/AccessCodeLayout.kt | 51 ------- 14 files changed, 244 insertions(+), 197 deletions(-) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index 1d192ffae5..b7eb62a719 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -9,6 +9,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.settings.ShouldAskPermissionUseCase import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute @@ -78,23 +79,23 @@ internal class AddExistingWalletModel @Inject constructor( } inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks { - override fun onWalletImported() { - stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted) + override fun onWalletImported(userWalletId: UserWalletId) { + stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted(userWalletId)) } } inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { - override fun onContinueClick() { - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode) + override fun onContinueClick(userWalletId: UserWalletId) { + stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode(userWalletId)) } } inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { - override fun onAccessCodeSet(accessCode: String) { - stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(accessCode)) + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode)) } - override fun onAccessCodeConfirmed() { + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { navigateToPushNotificationsOrNext() } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 90ffa797d0..2b68c1b132 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -37,6 +37,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( is AddExistingWalletRoute.BackupCompleted -> ManualBackupCompletedComponent( context = childContext, params = ManualBackupCompletedComponent.Params( + userWalletId = route.userWalletId, callbacks = model.manualBackupCompletedComponentModelCallbacks, ), ) @@ -44,6 +45,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( context = childContext, params = AccessCodeComponent.Params( isConfirmMode = false, + userWalletId = route.userWalletId, callbacks = model.accessCodeModelCallbacks, ), ) @@ -52,6 +54,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( params = AccessCodeComponent.Params( isConfirmMode = true, accessCodeToConfirm = route.accessCode, + userWalletId = route.userWalletId, callbacks = model.accessCodeModelCallbacks, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt index f1102b80eb..c5f3009aac 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletRoute.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.addexistingwallet.entry.routing import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable internal sealed class AddExistingWalletRoute : Route { @@ -12,13 +13,13 @@ internal sealed class AddExistingWalletRoute : Route { object Import : AddExistingWalletRoute() @Serializable - object BackupCompleted : AddExistingWalletRoute() + data class BackupCompleted(val userWalletId: UserWalletId) : AddExistingWalletRoute() @Serializable - object SetAccessCode : AddExistingWalletRoute() + data class SetAccessCode(val userWalletId: UserWalletId) : AddExistingWalletRoute() @Serializable - data class ConfirmAccessCode(val accessCode: String) : AddExistingWalletRoute() + data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : AddExistingWalletRoute() @Serializable object PushNotifications : AddExistingWalletRoute() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt index afd76f5987..096bd395a0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/AddExistingWalletImportComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent import dagger.assisted.Assisted @@ -28,7 +29,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor( } interface ModelCallbacks { - fun onWalletImported() + fun onWalletImported(userWalletId: UserWalletId) } data class Params( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 2f70fa2273..f7664bfa55 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -4,13 +4,19 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.wallets.builder.HotUserWalletBuilder +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -18,6 +24,9 @@ internal class AddExistingWalletImportModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val mnemonicRepository: MnemonicRepository, + private val tangemHotSdk: TangemHotSdk, + private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, + private val saveUserWalletUseCase: SaveWalletUseCase, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() @@ -44,7 +53,24 @@ internal class AddExistingWalletImportModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { - // TODO implement importing seed phrase - params.callbacks.onWalletImported() + modelScope.launch { + uiState.update { + it.copy(createWalletProgress = true) + } + + runCatching { + val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) + val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase(userWallet) + params.callbacks.onWalletImported(userWallet.walletId) + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(createWalletProgress = false) + } + } + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 22cb0309c7..33e7872bb8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -35,7 +35,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.OutlineTextFieldWithIcon -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState @@ -91,12 +91,11 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth(), text = stringResourceSafe(id = R.string.common_import), - iconResId = R.drawable.ic_tangem_24, enabled = state.createWalletEnabled, showProgress = state.createWalletProgress, onClick = state.createWalletClick, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt index 43f8c076d3..bd40e6bb1a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -27,10 +28,11 @@ internal class ManualBackupCompletedComponent @AssistedInject constructor( } interface ModelCallbacks { - fun onContinueClick() + fun onContinueClick(userWalletId: UserWalletId) } data class Params( + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt index 0646d7646b..d0fcfe7333 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt @@ -20,7 +20,7 @@ internal class ManualBackupCompletedModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( ManualBackupCompletedUM( - onContinueClick = params.callbacks::onContinueClick, + onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) }, ), ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt index 4200b68d42..6cd119cdfb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt @@ -8,9 +8,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.hotwallet.setaccesscode.ui.AccessCodeLayout -import com.tangem.core.res.R +import com.tangem.features.hotwallet.setaccesscode.ui.AccessCode +import com.tangem.domain.models.wallet.UserWalletId import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -28,32 +27,21 @@ internal class AccessCodeComponent @AssistedInject constructor( DisableScreenshotsDisposableEffect() - AccessCodeLayout( + AccessCode( modifier = modifier, - accessCode = state.accessCode, - onAccessCodeChange = state.onAccessCodeChange, - accessCodeLength = state.accessCodeLength, - reEnterAccessCodeState = params.isConfirmMode, - buttonText = stringResourceSafe( - if (params.isConfirmMode) { - R.string.common_confirm - } else { - R.string.common_continue - }, - ), - onButtonClick = state.onButtonClick, - buttonEnabled = state.buttonEnabled, + state = state, ) } interface ModelCallbacks { - fun onAccessCodeSet(accessCode: String) - fun onAccessCodeConfirmed() + fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) + fun onAccessCodeConfirmed(userWalletId: UserWalletId) } data class Params( val isConfirmMode: Boolean, val accessCodeToConfirm: String? = null, + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt index d978ba32f6..75210c3f64 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt @@ -1,14 +1,24 @@ package com.tangem.features.hotwallet.setaccesscode import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.setaccesscode.entity.AccessCodeUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -16,6 +26,9 @@ import javax.inject.Inject internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val saveWalletUseCase: SaveWalletUseCase, + private val tangemHotSdk: TangemHotSdk, ) : Model() { private val params = paramsContainer.require() @@ -26,7 +39,9 @@ internal class AccessCodeModel @Inject constructor( private fun getInitialState() = AccessCodeUM( accessCode = "", onAccessCodeChange = ::onAccessCodeChange, + isConfirmMode = params.isConfirmMode, buttonEnabled = false, + buttonInProgress = false, onButtonClick = ::onButtonClick, ) @@ -44,10 +59,40 @@ internal class AccessCodeModel @Inject constructor( } private fun onButtonClick() { - if (params.isConfirmMode) { - params.callbacks.onAccessCodeConfirmed() + if (!params.isConfirmMode) { + params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode) } else { - params.callbacks.onAccessCodeSet(uiState.value.accessCode) + params.accessCodeToConfirm?.let { + setCode(params.userWalletId, it) + } + } + } + + private fun setCode(userWalletId: UserWalletId, accessCode: String) { + modelScope.launch { + uiState.update { + it.copy(buttonInProgress = true) + } + + runCatching { + val userWallet = getUserWalletUseCase(userWalletId) + .getOrElse { error("User wallet with id $userWalletId not found") } + if (userWallet is UserWallet.Hot) { + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId)) + params.callbacks.onAccessCodeConfirmed(params.userWalletId) + } + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(buttonInProgress = false) + } + } } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt index 836a816175..9141f7f4c5 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt @@ -5,7 +5,9 @@ import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( val accessCode: String, val onAccessCodeChange: (String) -> Unit, + val isConfirmMode: Boolean, val buttonEnabled: Boolean, + val buttonInProgress: Boolean, val onButtonClick: () -> Unit, ) { val accessCodeLength: Int = ACCESS_CODE_LENGTH diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt new file mode 100644 index 0000000000..923b475395 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt @@ -0,0 +1,136 @@ +package com.tangem.features.hotwallet.setaccesscode.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.setaccesscode.entity.AccessCodeUM + +@Suppress("LongParameterList", "LongMethod") +@Composable +internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + Column( + modifier = Modifier + .padding(top = 16.dp) + .weight(1f) + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .padding(top = 56.dp) + .align(Alignment.CenterHorizontally), + text = if (state.isConfirmMode) { + stringResourceSafe(R.string.access_code_confirm_title) + } else { + stringResourceSafe(R.string.access_code_create_title) + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterHorizontally), + text = if (state.isConfirmMode) { + stringResourceSafe(R.string.access_code_confirm_description) + } else { + stringResourceSafe( + R.string.access_code_create_description, + state.accessCodeLength, + ) + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + contentAlignment = Alignment.Center, + ) { + PinTextField( + length = state.accessCodeLength, + isPasswordVisual = true, + value = state.accessCode, + onValueChange = state.onAccessCodeChange, + ) + } + } + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + text = stringResourceSafe( + if (state.isConfirmMode) { + R.string.common_confirm + } else { + R.string.common_continue + }, + ), + onClick = state.onButtonClick, + enabled = state.buttonEnabled, + showProgress = state.buttonInProgress, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewSet() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "", + onAccessCodeChange = {}, + isConfirmMode = false, + buttonEnabled = false, + buttonInProgress = false, + onButtonClick = {}, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewConfirm() { + TangemThemePreview { + AccessCode( + state = AccessCodeUM( + accessCode = "123456", + onAccessCodeChange = {}, + isConfirmMode = true, + buttonEnabled = true, + buttonInProgress = false, + onButtonClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt deleted file mode 100644 index 62e4388d9a..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeEnter.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.res.R -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -internal fun AccessCodeEnter( - accessCode: String, - onAccessCodeChange: (String) -> Unit, - accessCodeLength: Int, - reEnterAccessCodeState: Boolean, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.primary) - .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - modifier = Modifier - .padding(top = 56.dp) - .align(Alignment.CenterHorizontally), - text = if (reEnterAccessCodeState) { - stringResourceSafe(R.string.access_code_confirm_title) - } else { - stringResourceSafe(R.string.access_code_create_title) - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Text( - modifier = Modifier - .padding(16.dp) - .align(Alignment.CenterHorizontally), - text = if (reEnterAccessCodeState) { - stringResourceSafe(R.string.access_code_confirm_description) - } else { - stringResourceSafe( - R.string.access_code_create_description, - accessCodeLength, - ) - }, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = accessCodeLength, - isPasswordVisual = true, - value = accessCode, - onValueChange = onAccessCodeChange, - ) - } - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - AccessCodeEnter( - accessCode = "123456", - onAccessCodeChange = {}, - accessCodeLength = 6, - reEnterAccessCodeState = false, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview2() { - TangemThemePreview { - AccessCodeEnter( - accessCode = "123456", - onAccessCodeChange = {}, - accessCodeLength = 6, - reEnterAccessCodeState = true, - ) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt deleted file mode 100644 index aee581c976..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCodeLayout.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton - -@Suppress("LongParameterList") -@Composable -internal fun AccessCodeLayout( - accessCode: String, - onAccessCodeChange: (String) -> Unit, - accessCodeLength: Int, - reEnterAccessCodeState: Boolean, - buttonText: String, - onButtonClick: () -> Unit, - buttonEnabled: Boolean, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - ) { - AccessCodeEnter( - modifier = Modifier - .padding(top = 16.dp) - .weight(1f), - accessCode = accessCode, - onAccessCodeChange = onAccessCodeChange, - accessCodeLength = accessCodeLength, - reEnterAccessCodeState = reEnterAccessCodeState, - ) - - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .imePadding(), - text = buttonText, - onClick = onButtonClick, - enabled = buttonEnabled, - ) - } -} \ No newline at end of file From d0f1e7c6c45defc554fedd3f6d380b7a47df546e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 14:12:12 +0300 Subject: [PATCH 024/165] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 12 +++- .../features/tester/api/KeyEventObserver.kt | 13 ++++ .../features/tester/api/TesterMenuLauncher.kt | 9 +-- .../tester/di/TesterMenuLauncherModule.kt | 10 +-- .../tester/presentation/TesterActivity.kt | 2 + .../presentation/menu/ui/TesterMenuScreen.kt | 4 +- .../navigation/DefaultTesterMenuLauncher.kt | 46 ++----------- .../navigation/ShakeEventListener.kt | 51 --------------- .../VolumeButtonDoublePressObserver.kt | 64 +++++++++++++++++++ 9 files changed, 105 insertions(+), 106 deletions(-) create mode 100644 features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 7be16eab0a..526a308532 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.view.KeyEvent import android.view.MotionEvent import android.view.WindowManager import androidx.activity.SystemBarStyle @@ -241,7 +242,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { lifecycle.addObserver(defaultDeviceFlipDetector) if (BuildConfig.TESTER_MENU_ENABLED) { - lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) + lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver) } } @@ -417,10 +418,17 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun dispatchTouchEvent(event: MotionEvent): Boolean { val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) - return if (result) super.dispatchTouchEvent(event) else false } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + return if (BuildConfig.TESTER_MENU_ENABLED) { + testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) + } else { + super.dispatchKeyEvent(event) + } + } + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt new file mode 100644 index 0000000000..d431c7cd85 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/KeyEventObserver.kt @@ -0,0 +1,13 @@ +package com.tangem.features.tester.api + +import android.view.KeyEvent +import androidx.lifecycle.DefaultLifecycleObserver + +/** + * Interface for observing to key events in a lifecycle-aware manner. + * Implementations should handle key events and return true if the event was consumed. + */ +interface KeyEventObserver : DefaultLifecycleObserver { + + fun dispatchKeyEvent(event: KeyEvent): Boolean +} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt index a6338eaa73..b16f8b366d 100644 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt @@ -1,7 +1,5 @@ package com.tangem.features.tester.api -import androidx.lifecycle.DefaultLifecycleObserver - /** * Interface for launching the tester menu * @@ -9,6 +7,9 @@ import androidx.lifecycle.DefaultLifecycleObserver */ interface TesterMenuLauncher { - /** Observer for detecting shake events and launching the tester menu */ - val launchOnShakeObserver: DefaultLifecycleObserver + /** + * Observer for key events to open the tester menu. + * Implementations should handle key events and return true if the event was consumed. + */ + val launchOnKeyEventObserver: KeyEventObserver } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt index 859469177f..48e8ef8bfd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt @@ -6,15 +6,15 @@ import com.tangem.features.tester.api.TesterMenuLauncher import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext @Module -@InstallIn(SingletonComponent::class) +@InstallIn(ActivityComponent::class) internal object TesterMenuLauncherModule { @Provides - fun provideTesterMenuLauncher(@ApplicationContext context: Context): TesterMenuLauncher { - return DefaultTesterMenuLauncher(context = context) + fun provideTesterMenuLauncher(@ActivityContext context: Context): TesterMenuLauncher { + return DefaultTesterMenuLauncher(context) } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 3c28e87b46..8cd7fd94f1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -92,6 +93,7 @@ internal class TesterActivity : ComposeActivity() { innerTesterRouter.open(route) }, ), + modifier = Modifier.systemBarsPadding(), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index da2e67b08d..b369752bd1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -31,11 +31,11 @@ import kotlinx.collections.immutable.toImmutableList */ @OptIn(ExperimentalFoundationApi::class) @Composable -internal fun TesterMenuScreen(state: TesterMenuUM) { +internal fun TesterMenuScreen(state: TesterMenuUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) LazyColumn( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt index fbd8a860ff..1c1fc90ee1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt @@ -1,53 +1,15 @@ package com.tangem.feature.tester.presentation.navigation import android.content.Context -import android.content.Intent -import android.hardware.Sensor -import android.hardware.SensorManager -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import com.tangem.feature.tester.presentation.TesterActivity import com.tangem.features.tester.api.TesterMenuLauncher /** - * Default implementation of [TesterMenuLauncher] that listens for shake events using the device's accelerometer. - * When a shake is detected, it opens the tester menu. + * Default implementation of [TesterMenuLauncher] that listens for double-press events + * of the volume down button. When a double press is detected, it opens the tester menu. * - * @param context the application context used to access system services - * -[REDACTED_AUTHOR] + * @param context the application context used to launch the tester menu */ internal class DefaultTesterMenuLauncher(private val context: Context) : TesterMenuLauncher { - override val launchOnShakeObserver: DefaultLifecycleObserver by lazy(LazyThreadSafetyMode.NONE) { - createObserver(context) - } - - private fun createObserver(context: Context): DefaultLifecycleObserver { - return object : DefaultLifecycleObserver { - private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) - private val shakeEventListener = ShakeEventListener(action = ::openTesterMenu) - - override fun onResume(owner: LifecycleOwner) { - accelerometer?.let { - sensorManager.registerListener( - /* listener = */ shakeEventListener, - /* sensor = */ it, - /* samplingPeriodUs = */ SensorManager.SENSOR_DELAY_NORMAL, - ) - } - } - - override fun onPause(owner: LifecycleOwner) { - sensorManager.unregisterListener(shakeEventListener) - } - } - } - - private fun openTesterMenu() { - val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - - context.startActivity(intent) - } + override val launchOnKeyEventObserver = VolumeButtonDoublePressObserver(context) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt deleted file mode 100644 index 133628c9ac..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.tester.presentation.navigation - -import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import kotlin.math.sqrt - -/** - * Listener for device shake events. - * - * This class implements [SensorEventListener] and is used to detect device shaking based on accelerometer data. - * When a shake is detected, the provided action is invoked. - * - * @property action lambda function to be called when a shake is detected - * -[REDACTED_AUTHOR] - */ -internal class ShakeEventListener(private val action: () -> Unit) : SensorEventListener { - - private var lastShakeTime = 0L - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit - - override fun onSensorChanged(event: SensorEvent?) { - if (event?.sensor?.type != Sensor.TYPE_ACCELEROMETER) return - - val acceleration = calculateAcceleration(event = event) - - val currentTime = System.currentTimeMillis() - val currentShakeInterval = currentTime - lastShakeTime - - if (acceleration > SHAKE_THRESHOLD && currentShakeInterval > SHAKE_INTERVAL_MS) { - lastShakeTime = currentTime - action() - } - } - - private fun calculateAcceleration(event: SensorEvent): Float { - val (x, y, z) = event.toXYZ() - - return sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH - } - - private fun SensorEvent.toXYZ() = Triple(values[0], values[1], values[2]) - - private companion object { - private const val SHAKE_THRESHOLD: Float = 12f - private const val SHAKE_INTERVAL_MS: Long = 1000 - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt new file mode 100644 index 0000000000..c683121653 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/VolumeButtonDoublePressObserver.kt @@ -0,0 +1,64 @@ + +package com.tangem.feature.tester.presentation.navigation + +import android.content.Context +import android.content.Intent +import android.os.SystemClock +import android.view.KeyEvent +import androidx.lifecycle.LifecycleOwner +import com.tangem.feature.tester.presentation.TesterActivity +import com.tangem.features.tester.api.KeyEventObserver + +/** + * A key event observer that listens for volume down button presses to open the tester menu. + * It requires two consecutive volume down presses within a specified interval to trigger the menu. + */ +internal class VolumeButtonDoublePressObserver(private val context: Context) : KeyEventObserver { + + private var lastVolumeDownTime = 0L + private var volumeDownCount = 0 + private var isReady = false + + override fun onResume(owner: LifecycleOwner) { + isReady = true + } + + override fun onPause(owner: LifecycleOwner) { + isReady = false + } + + /** + * Returns true if the tester menu was opened. + */ + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (!isReady) return false + + if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + val now = SystemClock.elapsedRealtime() + volumeDownCount = if (now - lastVolumeDownTime <= DOUBLE_PRESS_INTERVAL_MS) { + volumeDownCount + 1 + } else { + 1 + } + lastVolumeDownTime = now + + if (volumeDownCount == REQUIRED_PRESS_COUNT) { + volumeDownCount = 0 + openTesterMenu(context) + return true + } + } + + return false + } + + private fun openTesterMenu(context: Context) { + val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + companion object { + private const val DOUBLE_PRESS_INTERVAL_MS = 300L + private const val REQUIRED_PRESS_COUNT = 2 + } +} \ No newline at end of file From 71003c43dbe6e504ec347b580c6bbb0abd45a213 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 16:49:44 +0300 Subject: [PATCH 025/165] Updated on 2026-08-14 --- .../common/extensions/BaseTestCaseExt.kt | 15 +++ .../tangem/screens/DisclaimerPageObject.kt | 12 ++ .../kotlin/com/tangem/tests/StoriesTest.kt | 11 +- .../com/tangem/tests/TermsOfServiceTest.kt | 106 ++++++++++++++++++ .../core/ui/test/DisclaimerScreenTestTags.kt | 1 + .../disclaimer/impl/ui/DisclaimerScreen.kt | 8 +- 6 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt new file mode 100644 index 0000000000..bff5addff5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt @@ -0,0 +1,15 @@ +package com.tangem.common.extensions + +import com.tangem.common.BaseTestCase + +fun BaseTestCase.swipeToCloseApp() { + + device.uiDevice.swipe( + device.uiDevice.displayWidth / 2, + device.uiDevice.displayHeight / 2, + device.uiDevice.displayWidth / 2, + device.uiDevice.displayHeight / 30, + 15 + ) + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt index fa12033630..9e43faf303 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DisclaimerPageObject.kt @@ -3,9 +3,12 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.test.DisclaimerScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.features.disclaimer.impl.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen( @@ -13,6 +16,15 @@ class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) viewBuilderAction = { hasTestTag(DisclaimerScreenTestTags.SCREEN_CONTAINER) } ) { + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.disclaimer_title)) + } + + val webView: KNode = child { + hasTestTag(DisclaimerScreenTestTags.WEB_VIEW) + } + val acceptButton: KNode = child { hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 8fe8ee6203..63a4706cb0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -3,18 +3,19 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion -import com.tangem.screens.DisclaimerTestScreen -import com.tangem.screens.StoriesTestScreen -import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL +import com.tangem.screens.onDisclaimerScreen +import com.tangem.screens.onStoriesScreen import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.intent.KIntent +import org.junit.Test @HiltAndroidTest class StoriesTest : BaseTestCase() { - // @Test + @Test fun clickOnOrderButtonTest() = setupHooks().run { + val buyWalletUrl = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" onDisclaimerScreen { step("Click on 'Accept' button") { acceptButton.clickWithAssertion() @@ -27,7 +28,7 @@ class StoriesTest : BaseTestCase() { step("Assert: browser opened") { val expectedIntent = KIntent { hasAction(ACTION_VIEW) - hasData { toString().startsWith(NEW_BUY_WALLET_URL) } + hasData { toString().startsWith(buyWalletUrl) } } expectedIntent.intended() device.uiDevice.pressBack() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt new file mode 100644 index 0000000000..0e596d4528 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt @@ -0,0 +1,106 @@ +package com.tangem.tests + +import androidx.test.InstrumentationRegistry.getTargetContext +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeToCloseApp +import com.tangem.screens.onDisclaimerScreen +import com.tangem.screens.onStoriesScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class TermsOfServiceTest : BaseTestCase() { + + @AllureId("3573") + @DisplayName("ToS: success acceptance") + @Test + fun validateTermsOfServiceScreenTest() { + setupHooks().run { + val tosUrl = "https://tangem.com/tangem_tos.html" + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Assert 'Stories' screen is opened") { + onStoriesScreen { + scanButton.assertIsDisplayed() + orderButton.assertIsDisplayed()} + } + } + } + + @AllureId("3574") + @DisplayName("ToS: accept after app restart") + @Test + fun acceptTermsOfServiceAfterAppRestart() { + val packageName = getTargetContext().packageName + setupHooks().run { + val tosUrl = "https://tangem.com/tangem_tos.html" + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert WebView of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("'Accept' button is displayed") { + onDisclaimerScreen { acceptButton.assertIsDisplayed() } + } + step("Open recent apps") { + device.uiDevice.pressRecentApps() + } + step("Stop app by swipe") { + swipeToCloseApp() + } + step("Launch app") { + device.apps.launch(packageName) + } + step("Assert title of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { title.assertIsDisplayed() } + } + step("Assert WebView of 'Disclaimer screen' is displayed") { + onDisclaimerScreen { webView.assertIsDisplayed() } + } + step("Verify WebView loads correct URL") { + onDisclaimerScreen { + webView.assertContentDescriptionContains(value = tosUrl, substring = true) + } + } + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Open recent apps") { + device.uiDevice.pressRecentApps() + } + step("Stop app by swipe") { + swipeToCloseApp() + } + step("Launch app") { + device.apps.launch(packageName) + } + step("Assert 'Stories' screen is opened") { + onStoriesScreen { + scanButton.assertIsDisplayed() + orderButton.assertIsDisplayed()} + } + } + } + +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt index b1355ed20a..e5716b476f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/DisclaimerScreenTestTags.kt @@ -3,4 +3,5 @@ package com.tangem.core.ui.test object DisclaimerScreenTestTags { const val SCREEN_CONTAINER = "DISCLAIMER_SCREEN_CONTAINER" const val ACCEPT_BUTTON = "DISCLAIMER_SCREEN_ACCEPT_BUTTON" + const val WEB_VIEW = "DISCLAIMER_SCREEN_WEB_VIEW" } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 4a68dba57e..46e17998f8 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted @@ -111,7 +113,11 @@ private fun DisclaimerContent(url: String) { state = webViewState, modifier = Modifier .fillMaxSize() - .background(TangemTheme.colors.background.primary), + .background(TangemTheme.colors.background.primary) + .testTag(DisclaimerScreenTestTags.WEB_VIEW) + .semantics { + contentDescription = "WebView URL: ${webViewState.content.getCurrentUrl() ?: url}" + }, captureBackPresses = false, navigator = webViewNavigator, onCreated = WebView::applySafeSettings, From 3cd5c708c9609538c11bdc449a4d448570c03206 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 15:46:06 +0400 Subject: [PATCH 026/165] Updated on 2026-08-14 --- .../tangem/domain/models/account/Account.kt | 127 ++++++++++++ .../tangem/domain/models/account/AccountId.kt | 14 ++ .../domain/models/account/AccountName.kt | 59 ++++++ .../models/account/CryptoPortfolioIcon.kt | 83 +++----- .../domain/models/account/AccountNameTest.kt | 67 +++++++ .../domain/models/account/AccountTest.kt | 187 ++++++++++++++++++ .../models/account/CryptoPortfolioIconTest.kt | 172 ++++++++-------- 7 files changed, 564 insertions(+), 145 deletions(-) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt new file mode 100644 index 0000000000..398930b72e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -0,0 +1,127 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents an account + * +[REDACTED_AUTHOR] + */ +sealed interface Account { + + /** Unique identifier of the account */ + val accountId: AccountId + + /** Name of the account */ + val name: AccountName + + /** The identifier of the user wallet associated with the account */ + val userWalletId: UserWalletId + get() = accountId.userWalletId + + /** + * Represents a crypto portfolio account + * + * @property accountId unique identifier of the account + * @property name name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property isArchived indicates whether the account is archived + * @property cryptoCurrencyList list of tokens associated with the account + */ + data class CryptoPortfolio private constructor( + override val accountId: AccountId, + override val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: Int, + val isArchived: Boolean, + val cryptoCurrencyList: CryptoCurrencyList, + ) : Account { + + /** Indicates if the account is the main account */ + val isMainAccount: Boolean + get() = derivationIndex == 0 + + /** Number of tokens in the account */ + val tokensCount: Int + get() = cryptoCurrencyList.currencies.size + + /** Number of distinct networks in the account */ + val networksCount: Int + get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + + /** + * Represents a list of tokens in the account + * + * @property currencies set of cryptocurrencies in the account + * @property sortType sorting type for the tokens + * @property groupType grouping type for the tokens + */ + data class CryptoCurrencyList( + val currencies: Set, + val sortType: TokensSortType, + val groupType: TokensGroupType, + ) + + /** + * Represents possible errors when creating a crypto portfolio account + */ + sealed interface Error { + + /** Error indicating that the account name is blank */ + data class AccountNameError(val cause: AccountName.Error) : Error { + override fun toString(): String = cause.toString() + } + + /** Error indicating that the derivation index is negative */ + data object NegativeDerivationIndex : Error { + override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative" + } + } + + companion object { + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param name name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + name: String, + accountIcon: CryptoPortfolioIcon, + derivationIndex: Int, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): Either { + return either { + val accountName = AccountName(name).mapLeft(::AccountNameError).bind() + + ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex } + + CryptoPortfolio( + accountId = accountId, + name = accountName, + icon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + } + } + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt new file mode 100644 index 0000000000..2f5728579c --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents a unique identifier for an account + * + * @property value a unique string value that distinguishes this account + * @property userWalletId the identifier of the user wallet associated with the account + */ +data class AccountId( + val value: String, + val userWalletId: UserWalletId, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt new file mode 100644 index 0000000000..e51a8e5a30 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure + +/** + * Represents an account name + * + * @property value the validated account name as a string + * +[REDACTED_AUTHOR] + */ +data class AccountName private constructor( + val value: String, +) { + + /** + * Represents possible validation errors + */ + sealed interface Error { + + /** + * Error indicating that the account name is blank + */ + data object Empty : Error { + override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank" + } + + /** + * Error indicating that the account name exceeds the maximum allowed length + */ + data object ExceedsMaxLength : Error { + override fun toString(): String { + return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters" + } + } + } + + companion object { + + private const val MAX_LENGTH = 20 + + /** + * Factory method to create an `AccountName` instance. + * Validates the input string to ensure it is not blank and does not exceed the maximum length. + * + * @param value the input string representing the account name + */ + operator fun invoke(value: String): Either = either { + val trimmedValue = value.trim() + + ensure(trimmedValue.isNotBlank()) { Error.Empty } + ensure(trimmedValue.length <= MAX_LENGTH) { Error.ExceedsMaxLength } + + AccountName(value = trimmedValue) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt index 9846695785..15b906020b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CryptoPortfolioIcon.kt @@ -1,48 +1,26 @@ package com.tangem.domain.models.account -import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofCustomAccount +import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofDefaultCustomAccount import com.tangem.domain.models.account.CryptoPortfolioIcon.Companion.ofMainAccount +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable /** * Represents an icon for an [Account.CryptoPortfolio] account * - * @property type the type of the account icon + * @property value the type of the account icon * @property color the color of the account icon * - * @constructor [ofMainAccount], [ofCustomAccount] + * @constructor [ofMainAccount], [ofDefaultCustomAccount] * [REDACTED_AUTHOR] */ @Serializable data class CryptoPortfolioIcon private constructor( - val type: Type, + val value: Icon, val color: Color, ) { - /** - * Represents the type of an account icon. Can either be a specific [Icon] or a [Symbol] - */ - @Serializable - sealed interface Type { - - /** - * Represents a specific predefined icon type - * - * @property value the predefined [Icon] of the icon - */ - @Serializable - data class Icon(val value: CryptoPortfolioIcon.Icon) : Type - - /** - * Represents an icon with a letter - * - * @property value the letter used as the icon - */ - @Serializable - data class Symbol(val value: Char) : Type - } - /** * Enum class representing the icons of accounts */ @@ -91,59 +69,44 @@ data class CryptoPortfolioIcon private constructor( companion object { - private val defaultMainAccountType: Icon = Icon.Star - private val defaultMainAccountColor: Color = Color.Azure + private val defaultMainAccountIcon: Icon = Icon.Star + private val excludedCustomAccountIcons: Set = setOf(Icon.Letter, Icon.Star) + private const val HASH_MULTIPLIER = 31 /** - * Creates an [CryptoPortfolioIcon] for the Main account, ensuring the color is not in the excluded set. + * Creating a [CryptoPortfolioIcon] for the Main account with default values. + * The color is derived from the [UserWalletId]. * - * @param exclude excluded colors that are already used for main accounts + * @param userWalletId the ID of the user wallet */ - fun ofMainAccount(exclude: Set): CryptoPortfolioIcon { - val isDefaultColorBusy = defaultMainAccountColor in exclude + fun ofMainAccount(userWalletId: UserWalletId): CryptoPortfolioIcon { + val colors = Color.entries + val hash = userWalletId.value.fold(0) { acc, byte -> acc * HASH_MULTIPLIER + byte } - val color = if (isDefaultColorBusy) { - val colorsWithExcluded = Color.entries - exclude + val index = (hash and Int.MAX_VALUE) % colors.size + val color = colors[index] - val availableColors = if (colorsWithExcluded.isNotEmpty()) { - colorsWithExcluded - } else { - Color.entries - } - - availableColors.random() - } else { - defaultMainAccountColor - } - - return CryptoPortfolioIcon( - type = Type.Icon(value = defaultMainAccountType), - color = color, - ) + return CryptoPortfolioIcon(value = defaultMainAccountIcon, color = color) } /** * Creates an [CryptoPortfolioIcon] for a user account based on the account name - * - * @param accountName the name of the account, used to determine the letter for the icon */ - fun ofCustomAccount(accountName: String): CryptoPortfolioIcon { + fun ofDefaultCustomAccount(): CryptoPortfolioIcon { + val icon = (Icon.entries - excludedCustomAccountIcons).random() val color = Color.entries.random() - return CryptoPortfolioIcon( - type = Type.Symbol(value = accountName.first()), - color = color, - ) + return CryptoPortfolioIcon(value = icon, color = color) } /** * Creates a [CryptoPortfolioIcon] for a user account with a specific type and color * - * @param type the type of the account icon + * @param value the icon of the account * @param color the color of the account icon */ - fun ofCustomAccount(type: Type, color: Color): CryptoPortfolioIcon { - return CryptoPortfolioIcon(type = type, color = color) + fun ofCustomAccount(value: Icon, color: Color): CryptoPortfolioIcon { + return CryptoPortfolioIcon(value = value, color = color) } } } \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt new file mode 100644 index 0000000000..d68bc46ab9 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.left +import com.google.common.truth.Truth +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountNameTest { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: InvokeTestModel) { + // Act + val actual = AccountName(value = model.value) + + // Assert + actual + .onRight { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onLeft { + val expected = model.expected.leftOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + } + + private fun provideTestModels() = listOf( + InvokeTestModel( + value = "", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = " ", + expected = AccountName.Error.Empty.left(), + ), + InvokeTestModel( + value = "a".repeat(21), + expected = AccountName.Error.ExceedsMaxLength.left(), + ), + "a".repeat(20).let { value -> + InvokeTestModel( + value = value, + expected = AccountName(value = value), + ) + }, + InvokeTestModel( + value = " name ", + expected = AccountName(value = "name"), + ), + InvokeTestModel( + value = "Main Account", + expected = AccountName(value = "Main Account"), + ), + ) + + data class InvokeTestModel( + val value: String, + val expected: Either, + ) +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt new file mode 100644 index 0000000000..d94c1a6bdf --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -0,0 +1,187 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountTest { + + @Test + fun `Account userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + + // Act + val actual = createCryptoPortfolioStub(userWalletId = userWalletId).userWalletId + + // Assert + Truth.assertThat(actual).isEqualTo(userWalletId) + } + + @Test + fun `CryptoPortfolio isMainAccount`() { + // Arrange + val derivationIndex0 = 0 + val derivationIndex1 = 1 + + // Act + val actual1 = createCryptoPortfolioStub(derivationIndex = derivationIndex0) + .isMainAccount + + val actual2 = createCryptoPortfolioStub(derivationIndex = derivationIndex1) + .isMainAccount + + // Assert + Truth.assertThat(actual1).isTrue() + Truth.assertThat(actual2).isFalse() + } + + @Test + fun `CryptoPortfolio tokensCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf(mockk()) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .tokensCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .tokensCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Test + fun `CryptoPortfolio networksCount`() { + // Arrange + val emptyCurrencies = emptySet() + val filledCurrencies = setOf( + mockk { + every { network } returns mockk() + }, + ) + + // Act + val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) + .networksCount + + val actual2 = createCryptoPortfolioStub(currencies = filledCurrencies) + .networksCount + + // Assert + Truth.assertThat(actual1).isEqualTo(0) + Truth.assertThat(actual2).isEqualTo(1) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class CreateCryptoPortfolio { + + @Test + fun `invoke returns AccountNameError`() { + // Arrange + val name = "" + + // Act + val actual = Account.CryptoPortfolio( + accountId = mockk(), + name = name, + accountIcon = mockk(), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = mockk(), + ) + .leftOrNull()!! + + // Assert + val expected = AccountNameError(cause = AccountName.Error.Empty) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `invoke returns NegativeDerivationIndex`() { + // Arrange + val derivationIndex = -1 + + // Act + val actual = Account.CryptoPortfolio( + accountId = mockk(), + name = "Test Account", + accountIcon = mockk(), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = mockk(), + ) + .leftOrNull()!! + + // Assert + val expected = Account.CryptoPortfolio.Error.NegativeDerivationIndex + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `invoke returns CryptoPortfolio`() { + // Act + val actual = Account.CryptoPortfolio( + accountId = AccountId( + value = "value", + userWalletId = UserWalletId("011"), + ), + name = "Test Account", + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + + // Assert + val expected = createCryptoPortfolioStub() + Truth.assertThat(actual).isEqualTo(expected) + } + } + + private fun createCryptoPortfolioStub( + userWalletId: UserWalletId = UserWalletId("011"), + name: String = "Test Account", + derivationIndex: Int = 0, + currencies: Set = emptySet(), + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio.invoke( + accountId = AccountId( + value = "value", + userWalletId = userWalletId, + ), + name = name, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = currencies, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt index 3706e6921d..ec55aabdcd 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/CryptoPortfolioIconTest.kt @@ -1,13 +1,14 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.account.CryptoPortfolioIcon.* +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon +import com.tangem.domain.models.wallet.UserWalletId import io.mockk.every import io.mockk.mockkObject import io.mockk.unmockkObject -import io.mockk.verify +import io.mockk.verifyOrder import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -23,159 +24,160 @@ class CryptoPortfolioIconTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfMainAccount { - @Test - fun `ofMainAccount with empty exclude`() { - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = emptySet()) - - // Assert - val expectedColor = Color.Azure - Truth.assertThat(actual.color).isEqualTo(expectedColor) - - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - } - @ParameterizedTest @MethodSource("provideTestModels") fun ofMainAccount(model: OfMainAccountModel) { - // Arrange - mockkObject(Random.Default) - - val size = (Color.entries.size - model.exclude.size).takeIf { it > 0 } ?: Color.entries.size - every { Random.nextInt(size) } returns model.randomNextInt - // Act - val actual = CryptoPortfolioIcon.ofMainAccount(exclude = model.exclude) + val actual = CryptoPortfolioIcon.ofMainAccount(userWalletId = model.userWalletId) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) - val expectedType = Type.Icon(value = Icon.Star) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(size) } - - unmockkObject(Random.Default) + val expectedIcon = Icon.Star + Truth.assertThat(actual.value).isEqualTo(expectedIcon) } private fun provideTestModels() = listOf( - // If the default color is already occupied (present in the exclude set), a random color from the - // remaining available colors will be selected for the main account icon. OfMainAccountModel( - exclude = setOf(Color.Azure), - randomNextInt = 0, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("1234567890abcdef"), + expectedColor = Color.Pattypan, ), OfMainAccountModel( - exclude = setOf(Color.Azure, Color.CaribbeanBlue), - randomNextInt = 0, - expectedColor = Color.entries[2], + userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F"), + expectedColor = Color.CandyGrapeFizz, ), - // If all colors are already occupied, a random one will be selected. OfMainAccountModel( - exclude = Color.entries.toSet(), - randomNextInt = 1, - expectedColor = Color.entries[1], + userWalletId = UserWalletId("64A3791C180584C700EBECD6EAB36CBC34643BB449BC87761104C09F41DBCF3D"), + expectedColor = Color.PalatinateBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("01C061A99FCCEDA87933267EBAB3513592F83AD2E27BDA6EE5546BA96009D21F"), + expectedColor = Color.Pelati, + ), + OfMainAccountModel( + userWalletId = UserWalletId("6D387A8FA5D2AF95F601EBCA8736D73D2ED53159835D8C407FBD4BBB10290C8B"), + expectedColor = Color.CaribbeanBlue, + ), + OfMainAccountModel( + userWalletId = UserWalletId("33FCD9B9982C31648C235AE55A29212D567ECD3BA24BE4227D1A01897ADBC959"), + expectedColor = Color.SweetDesire, + ), + OfMainAccountModel( + userWalletId = UserWalletId("197C8C5AA59270F3E9E1F30799A007D193DA596E6DC24C37D002C2EC203C2A0B"), + expectedColor = Color.VitalGreen, + ), + OfMainAccountModel( + userWalletId = UserWalletId("ACF90C18393828958B5E795771F0692A00D3D7ADC092F726AB4A7E3116DD6E6E"), + expectedColor = Color.Pattypan, ), ) } data class OfMainAccountModel( - val exclude: Set, - val randomNextInt: Int, + val userWalletId: UserWalletId, val expectedColor: Color, ) @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) - inner class OfCustomAccountBasedOnName { + inner class OfDefaultCustomAccount { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.BasedOnName) { + fun ofCustomAccount(model: OfDefaultCustomAccountModel) { // Arrange + val availableIcons = Icon.entries - setOf(Icon.Letter, Icon.Star) + mockkObject(Random.Default) - every { Random.nextInt(until = Color.entries.size) } returns model.randomNextInt + every { Random.nextInt(until = availableIcons.size) } returns model.randomIconIndex + every { Random.nextInt(until = Color.entries.size) } returns model.randomColorIndex // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(accountName = model.accountName) + val actual = CryptoPortfolioIcon.ofDefaultCustomAccount() // Assert - val expectedColor = model.expectedColor - Truth.assertThat(actual.color).isEqualTo(expectedColor) + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) - val expectedType = Type.Symbol(value = model.accountName.first()) - Truth.assertThat(actual.type).isEqualTo(expectedType) - - verify(exactly = 1) { Random.nextInt(until = Color.entries.size) } + verifyOrder { + Random.nextInt(until = availableIcons.size) + Random.nextInt(until = Color.entries.size) + } unmockkObject(Random.Default) } private fun provideTestModels() = listOf( - OfCustomAccountModel.BasedOnName( - accountName = "New account", - randomNextInt = 0, - expectedColor = Color.entries[0], + OfDefaultCustomAccountModel( + randomIconIndex = 0, + randomColorIndex = 0, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.User, color = Color.Azure), ), - OfCustomAccountModel.BasedOnName( - accountName = "Awesome", - randomNextInt = Color.entries.lastIndex, - expectedColor = Color.entries.last(), + OfDefaultCustomAccountModel( + randomIconIndex = 1, + randomColorIndex = 1, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Family, color = Color.CaribbeanBlue), + ), + OfDefaultCustomAccountModel( + randomIconIndex = Icon.entries.lastIndex - 2, + randomColorIndex = Color.entries.lastIndex, + expected = CryptoPortfolioIcon.ofCustomAccount(value = Icon.Gift, color = Color.VitalGreen), ), ) } + data class OfDefaultCustomAccountModel( + val randomIconIndex: Int, + val randomColorIndex: Int, + val expected: CryptoPortfolioIcon, + ) + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class OfCustomAccountWithTypeAndColor { @ParameterizedTest @MethodSource("provideTestModels") - fun ofCustomAccount(model: OfCustomAccountModel.WithTypeAndColor) { + fun ofCustomAccount(model: OfCustomAccountModel) { // Act - val actual = CryptoPortfolioIcon.ofCustomAccount(type = model.type, color = model.color) + val actual = CryptoPortfolioIcon.ofCustomAccount(value = model.icon, color = model.color) // Assert val expectedColor = model.expectedColor Truth.assertThat(actual.color).isEqualTo(expectedColor) val expectedType = model.expectedType - Truth.assertThat(actual.type).isEqualTo(expectedType) + Truth.assertThat(actual.value).isEqualTo(expectedType) } private fun provideTestModels() = listOf( - OfCustomAccountModel.WithTypeAndColor( - type = Type.Icon(value = Icon.User), + OfCustomAccountModel( + icon = Icon.User, color = Color.CaribbeanBlue, - expectedType = Type.Icon(value = Icon.User), + expectedType = Icon.User, expectedColor = Color.CaribbeanBlue, ), - OfCustomAccountModel.WithTypeAndColor( - type = Type.Symbol(value = 'A'), + OfCustomAccountModel( + icon = Icon.Letter, color = Color.DullLavender, - expectedType = Type.Symbol(value = 'A'), + expectedType = Icon.Letter, + expectedColor = Color.DullLavender, + ), + OfCustomAccountModel( + icon = Icon.Star, + color = Color.DullLavender, + expectedType = Icon.Star, expectedColor = Color.DullLavender, ), ) } - sealed interface OfCustomAccountModel { - - data class BasedOnName( - val accountName: String, - val randomNextInt: Int, - val expectedColor: Color, - ) : OfCustomAccountModel - - data class WithTypeAndColor( - val type: Type, - val color: Color, - val expectedType: Type, - val expectedColor: Color, - ) : OfCustomAccountModel - } + data class OfCustomAccountModel( + val icon: Icon, + val color: Color, + val expectedType: Icon, + val expectedColor: Color, + ) } \ No newline at end of file From 4be7763d712345178a4a2f9325a6c0006587435a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 08:12:44 +0000 Subject: [PATCH 027/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..e3fa514003 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1125" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-505" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-446" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From 49800731e347fe34a1751a4d3bf10b73f3cfd27e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Jul 2025 16:22:11 +0500 Subject: [PATCH 028/165] Updated on 2026-08-14 --- ...TransactionStatusStoreModule.kt => SwapStoreModule.kt} | 8 +++++--- .../{swaptx => swap}/DefaultSwapTransactionStatusStore.kt | 2 +- .../local/{swaptx => swap}/SwapTransactionStatusStore.kt | 2 +- .../state/factory/express/ExchangeStatusFactory.kt | 4 ++-- .../state/factory/express/OnrampStatusFactory.kt | 2 +- .../presentation/wallet/domain/OnrampStatusFactory.kt | 2 +- 6 files changed, 11 insertions(+), 9 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/di/{SwapTransactionStatusStoreModule.kt => SwapStoreModule.kt} (62%) rename core/datasource/src/main/java/com/tangem/datasource/local/{swaptx => swap}/DefaultSwapTransactionStatusStore.kt (91%) rename core/datasource/src/main/java/com/tangem/datasource/local/{swaptx => swap}/SwapTransactionStatusStore.kt (92%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt similarity index 62% rename from core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt index 25f71c60a0..edf8c36cc3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SwapTransactionStatusStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore -import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore +import com.tangem.datasource.local.swap.DefaultSwapBestRateAnimationStore +import com.tangem.datasource.local.swap.DefaultSwapTransactionStatusStore +import com.tangem.datasource.local.swap.SwapBestRateAnimationStore +import com.tangem.datasource.local.swap.SwapTransactionStatusStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,7 +13,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -object SwapTransactionStatusStoreModule { +object SwapStoreModule { @Provides @Singleton diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt similarity index 91% rename from core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt index bb094d028f..87881b3768 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/DefaultSwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapTransactionStatusStore.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.swaptx +package com.tangem.datasource.local.swap import com.tangem.datasource.local.datastore.core.StringKeyDataStore diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt index 033f71b6ac..af554bba1a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapTransactionStatusStore.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.local.swaptx +package com.tangem.datasource.local.swap /** * Runtime cache for storing swap transactions statuses sent to analytics diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index d418812ee0..27a03a1006 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -2,8 +2,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus -import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index 2e91b8b73d..61a05030ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 6c985e4728..2af73a2491 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase From d664e6ecbb5cc689c4cacbe22df1b0bb077b9ffa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Jul 2025 16:26:32 +0500 Subject: [PATCH 029/165] Updated on 2026-08-14 --- .../tangem/datasource/di/SwapStoreModule.kt | 9 + .../swap/DefaultSwapBestRateAnimationStore.kt | 21 ++ .../local/swap/SwapBestRateAnimationStore.kt | 11 + features/swap-v2/impl/build.gradle.kts | 1 + .../impl/amount/SwapAmountBlockComponent.kt | 1 + .../v2/impl/amount/entity/SwapAmountUM.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 14 ++ .../SwapAmountPrimaryReadyStateTransformer.kt | 2 + ...wapAmountSecondaryReadyStateTransformer.kt | 2 + .../SwapAmountSetQuotesTransformer.kt | 2 +- .../impl/amount/ui/SwapAmountBlockContent.kt | 10 +- .../ui/preview/SwapAmountContentPreview.kt | 2 + .../ui/SwapChooseProviderContent.kt | 230 +++++++++++++++--- 13 files changed, 267 insertions(+), 39 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt index edf8c36cc3..682bf87475 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SwapStoreModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.swap.DefaultSwapBestRateAnimationStore import com.tangem.datasource.local.swap.DefaultSwapTransactionStatusStore import com.tangem.datasource.local.swap.SwapBestRateAnimationStore @@ -22,4 +23,12 @@ object SwapStoreModule { dataStore = RuntimeDataStore(), ) } + + @Provides + @Singleton + fun provideSwapBestRateAnimationStore(): SwapBestRateAnimationStore { + return DefaultSwapBestRateAnimationStore( + dataStore = RuntimeSharedStore(), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt new file mode 100644 index 0000000000..4f207731c1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/DefaultSwapBestRateAnimationStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.swap + +import com.tangem.datasource.local.datastore.RuntimeSharedStore + +internal class DefaultSwapBestRateAnimationStore( + private val dataStore: RuntimeSharedStore, +) : SwapBestRateAnimationStore, RuntimeSharedStore by dataStore { + /** + * Returns flag indicating whether should show best rate animation in current session. + * Animation should appear once per session + * + * If true, reset flag to false + */ + override suspend fun getSyncOrNull(): Boolean { + val value = dataStore.getSyncOrNull() ?: true + if (value) { + dataStore.store(false) + } + return value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt new file mode 100644 index 0000000000..7fc8e8a1d0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swap/SwapBestRateAnimationStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.swap + +/** + * Stores flag indicating whether should show best rate animation in current session. + * Animation should appear once per session + * + * If true, reset flag to false + */ +interface SwapBestRateAnimationStore { + suspend fun getSyncOrNull(): Boolean +} \ No newline at end of file diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index c1d107eaf9..5cb929fe7e 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.navigation) implementation(projects.core.configToggles) + implementation(projects.core.datasource) /** Common */ implementation(projects.common.ui) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt index 5917c12bf3..0d6966910d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -68,6 +68,7 @@ internal class SwapAmountBlockComponent( onInfoClick = model::onInfoClick, isClickEnabled = isClickEnabled, onClick = onClick, + onFinishAnimation = model::onFinishAnimation, onProviderSelectClick = { val amountUM = model.uiState.value as? SwapAmountUM.Content ?: return@SwapAmountBlockContent val selectedProvider = amountUM.selectedQuote.provider ?: return@SwapAmountBlockContent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index fea9701d97..4c077efd90 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -51,6 +51,7 @@ internal sealed class SwapAmountUM { // extra data val appCurrency: AppCurrency?, + val showBestRateAnimation: Boolean, ) : SwapAmountUM() } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index f1d6be3848..149ccd0de1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError @@ -73,6 +74,7 @@ internal class SwapAmountModel @Inject constructor( private val getAllowanceUseCase: GetAllowanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserCountryUseCase: GetUserCountryUseCase, + private val swapBestRateAnimationStore: SwapBestRateAnimationStore, private val appRouter: AppRouter, private val swapAmountAlertFactory: SwapAmountAlertFactory, private val swapAlertFactory: SwapAlertFactory, @@ -96,6 +98,8 @@ internal class SwapAmountModel @Inject constructor( var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var showBestRateAnimation: Boolean = false + val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -107,6 +111,7 @@ internal class SwapAmountModel @Inject constructor( appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } userCountry = getUserCountryUseCase.invokeSync().getOrNull() ?: UserCountry.Other(Locale.getDefault().country) + showBestRateAnimation = swapBestRateAnimationStore.getSyncOrNull() } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() @@ -245,6 +250,12 @@ internal class SwapAmountModel @Inject constructor( } } + fun onFinishAnimation() { + uiState.update { + (it as? SwapAmountUM.Content)?.copy(showBestRateAnimation = false) ?: it + } + } + private fun confirmSendWithSwapClose() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data @@ -259,6 +270,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) } @@ -292,6 +304,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) } @@ -399,6 +412,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, clickIntents = this@SwapAmountModel, isBalanceHidden = params.isBalanceHidingFlow.value, + showBestRateAnimation = showBestRateAnimation, ), ) startLoadingQuotesTask(isSilentReload = false) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index 9aeeb9f2d7..f6c07b51ce 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -23,6 +23,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, + private val showBestRateAnimation: Boolean, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( @@ -52,6 +53,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, + showBestRateAnimation = showBestRateAnimation, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 9f54cc94b6..729519512d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -24,6 +24,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, + private val showBestRateAnimation: Boolean, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( @@ -51,6 +52,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, + showBestRateAnimation = showBestRateAnimation, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 614f1d8b53..665a6a37fb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -29,7 +29,7 @@ internal class SwapAmountSetQuotesTransformer( val selectedQuote = if (isSilentReload) { prevState.selectedQuote } else { - bestQuote + (bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index 291627de98..e2a6522a74 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,8 +37,9 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -@Suppress("DestructuringDeclarationWithTooManyEntries") +@Suppress("DestructuringDeclarationWithTooManyEntries", "LongParameterList") @Composable internal fun SwapAmountBlockContent( amountUM: SwapAmountUM, @@ -46,6 +47,7 @@ internal fun SwapAmountBlockContent( onProviderSelectClick: () -> Unit, onInfoClick: () -> Unit, onClick: () -> Unit, + onFinishAnimation: () -> Unit, modifier: Modifier = Modifier, ) { if (amountUM !is SwapAmountUM.Content) return @@ -98,9 +100,14 @@ internal fun SwapAmountBlockContent( end.linkTo(parent.end) }, ) + val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content + val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( + isBestRate = isBestRate, + showBestRateAnimation = amountUM.showBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, + onFinishAnimation = onFinishAnimation, modifier = Modifier.constrainAs(provider) { top.linkTo(to.bottom) bottom.linkTo(parent.bottom) @@ -195,6 +202,7 @@ private fun SwapAmountBlockContent_Preview() { onProviderSelectClick = {}, onInfoClick = {}, onClick = {}, + onFinishAnimation = {}, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index e7bf485c35..df8d9da742 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -88,6 +88,7 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, + showBestRateAnimation = false, ) val defaultState = SwapAmountUM.Content( @@ -124,5 +125,6 @@ internal data object SwapAmountContentPreview { secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, + showBestRateAnimation = false, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 38a3b4718c..2ef78b6506 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -1,6 +1,9 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -11,6 +14,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,6 +26,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope +import androidx.constraintlayout.compose.Visibility import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer @@ -33,9 +42,17 @@ import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.R +import kotlinx.coroutines.delay @Composable -fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> Unit, modifier: Modifier = Modifier) { +fun SwapChooseProviderContent( + expressProvider: ExpressProvider?, + isBestRate: Boolean, + showBestRateAnimation: Boolean, + onClick: () -> Unit, + onFinishAnimation: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier.clickable( interactionSource = remember { MutableInteractionSource() }, @@ -48,57 +65,193 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () -> color = TangemTheme.colors.stroke.primary, modifier = Modifier.padding(horizontal = 12.dp), ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = modifier.padding(12.dp), - ) { + Row(verticalAlignment = Alignment.CenterVertically) { Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_stack_new_24), ), tint = TangemTheme.colors.icon.accent, contentDescription = null, + modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 12.dp), ) Text( text = stringResourceSafe(R.string.express_provider), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(start = 8.dp), + modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 12.dp), ) SpacerWMax() - SubcomposeAsyncImage( - modifier = modifier - .size(20.dp) - .clip(RoundedCornerShape(4.dp)), - model = ImageRequest.Builder(context = LocalContext.current) - .data(expressProvider?.imageLarge) - .crossfade(enable = true) - .allowHardware(false) - .build(), - loading = { RectangleShimmer(radius = 4.dp) }, - error = { - Box( - modifier = Modifier.background( - color = TangemColorPalette.Light1, - shape = RoundedCornerShape(4.dp), - ), - ) + ProviderInfo(expressProvider, isBestRate, showBestRateAnimation, onFinishAnimation) + } + } +} + +@Composable +private fun ProviderInfo( + expressProvider: ExpressProvider?, + isBestRate: Boolean, + showBestRateAnimation: Boolean, + onFinishAnimation: () -> Unit, +) { + ConstraintLayout { + val (imageRef, nameRef, iconRef) = createRefs() + SubcomposeAsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(expressProvider?.imageLarge) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = 4.dp) }, + error = { + Box( + modifier = Modifier.background( + color = TangemColorPalette.Light1, + shape = RoundedCornerShape(4.dp), + ), + ) + }, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clip(RoundedCornerShape(4.dp)) + .constrainAs(imageRef) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) }, - contentDescription = null, - ) + ) + Text( + text = expressProvider?.name.orEmpty(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(start = 6.dp) + .constrainAs(nameRef) { + start.linkTo(imageRef.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + ) + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_chevron_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + modifier = Modifier + .padding(start = 4.dp) + .constrainAs(iconRef) { + start.linkTo(nameRef.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end, 12.dp) + }, + ) + BestRateBadge( + showBestRateAnimation = showBestRateAnimation, + isBestRate = isBestRate, + ref = imageRef, + onFinishAnimation = onFinishAnimation, + ) + } +} + +@Suppress("MagicNumber", "LongMethod") +@Composable +private fun ConstraintLayoutScope.BestRateBadge( + showBestRateAnimation: Boolean, + isBestRate: Boolean, + ref: ConstrainedLayoutReference, + onFinishAnimation: () -> Unit, + modifier: Modifier = Modifier, +) { + val animateState = remember { MutableTransitionState(false) } + + LaunchedEffect(showBestRateAnimation) { + if (showBestRateAnimation) { + delay(600L) + animateState.targetState = true + delay(1_500L) + animateState.targetState = false + onFinishAnimation() + } + } + + val iconSize by animateDpAsState( + label = "iconSize", + targetValue = if (animateState.targetState) { + 12.dp + } else { + 8.dp + }, + ) + val iconVerticalPaddings by animateDpAsState( + label = "iconVerticalPaddings", + targetValue = if (animateState.targetState) { + 3.dp + } else { + 2.dp + }, + ) + val iconHorizontalPaddings by animateDpAsState( + label = "iconHorizontalPaddings", + targetValue = if (animateState.targetState) { + 4.dp + } else { + 2.dp + }, + ) + + val startMargin by animateDpAsState( + label = "startMargin", + targetValue = if (animateState.targetState) { + (-12).dp + } else { + (-10).dp + }, + ) + + val topMargin by animateDpAsState( + label = "topMargin", + targetValue = if (animateState.targetState) { + (-12).dp + } else { + (-9).dp + }, + ) + + Row( + modifier = modifier + .constrainAs(createRef()) { + start.linkTo(ref.end, startMargin) + top.linkTo(ref.bottom, topMargin) + visibility = if (isBestRate) Visibility.Visible else Visibility.Gone + } + .background(TangemTheme.colors.stroke.transparency, RoundedCornerShape(120.dp)) + .padding(1.5.dp) + .background(TangemTheme.colors.icon.accent, RoundedCornerShape(120.dp)), + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_rounded_star_24), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.constant, + modifier = Modifier + .padding(iconHorizontalPaddings, iconVerticalPaddings) + .size(iconSize), + ) + AnimatedVisibility( + visibleState = animateState, + enter = expandIn() + fadeIn(), + exit = shrinkOut() + fadeOut(), + label = "textAnimation", + modifier = Modifier.padding(end = 6.dp), + ) { Text( - text = expressProvider?.name.orEmpty(), // todo provider error - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(start = 6.dp), - ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_chevron_24), - ), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - modifier = Modifier.padding(start = 4.dp), + text = stringResourceSafe(R.string.express_provider_best_rate), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.constantWhite, ) } } @@ -114,6 +267,8 @@ private fun SwapChooseProviderContent_Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) { SwapChooseProviderContent( + isBestRate = true, + showBestRateAnimation = true, expressProvider = ExpressProvider( providerId = "changelly", rateTypes = listOf(ExpressRateType.Fixed), @@ -126,6 +281,7 @@ private fun SwapChooseProviderContent_Preview() { slippage = null, ), onClick = {}, + onFinishAnimation = {}, ) } } From 14df650f1a0250682acbc139913de4779d0a47c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 12:52:08 +0300 Subject: [PATCH 030/165] Updated on 2026-08-14 --- .../tangem/tap/common/extensions/Analytics.kt | 2 +- .../tap/di/domain/TransactionDomainModule.kt | 14 +++++++++-- .../cardsettings/model/CardSettingsModel.kt | 3 ++- .../tangem/tap/features/main/MainViewModel.kt | 8 +----- .../welcome/redux/WelcomeMiddleware.kt | 2 +- .../moonpay/MoonPayService.kt | 8 +++--- .../DefaultManageTokensRepository.kt | 9 +++---- .../tangem/data/nft/DefaultNFTRepository.kt | 7 ++---- .../data/onramp/DefaultHotCryptoRepository.kt | 11 ++------ ...faultMultiWalletCryptoCurrenciesFetcher.kt | 3 +-- ...aultMultiWalletCryptoCurrenciesProducer.kt | 4 +-- .../derivations/MissedDerivationsFinder.kt | 2 +- .../data/wallets/hot/HotWalletAccessor.kt | 3 ++- .../domain/card/common/extensions/CardSdk.kt | 14 +++++++++++ ...FilterAvailableNetworksForWalletUseCase.kt | 24 ++++++------------ .../usecase/PrepareForSendUseCase.kt | 12 ++++++--- .../domain/transaction/usecase/SignUseCase.kt | 25 ++++++++++++------- .../wallets/builder/HotUserWalletBuilder.kt | 2 +- .../wallets/extension/UserWalletExtensions.kt | 3 ++- .../features/details/model/DetailsModel.kt | 20 +++++++++------ .../v2/visa/impl/model/OnboardingVisaModel.kt | 3 ++- .../selecttoken/model/OnrampOperationModel.kt | 5 ++-- .../tokenlist/model/OnrampTokenListModel.kt | 2 -- .../v2/send/confirm/model/SendConfirmModel.kt | 7 +++++- .../features/send/v2/send/model/SendModel.kt | 6 ++++- .../confirm/model/NFTSendConfirmModel.kt | 7 +++++- .../send/v2/sendnft/model/NFTSendModel.kt | 6 ++++- .../impl/presentation/model/StakingModel.kt | 4 +++ .../swap/v2/impl/common/SwapAlertFactory.kt | 6 ++++- .../tangem/feature/swap/model/SwapModel.kt | 9 +++++-- .../model/WalletSettingsModel.kt | 9 +++---- .../intents/WalletWarningsClickIntents.kt | 17 ++++++++++--- .../UpdateWalletCardsCountTransformer.kt | 5 ++-- .../state/utils/WalletLoadingStateFactory.kt | 4 +-- 34 files changed, 161 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index f2e7d342ef..e09ba24e78 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -24,7 +24,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) { fun Analytics.setContext(userWallet: UserWallet) { setUserId(userWallet.walletId.stringValue) - // TODO add product type for hot ([REDACTED_TASK_KEY]) + // TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics) if (userWallet is UserWallet.Cold) { addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 3538d33584..ffd34ca773 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -180,8 +180,13 @@ internal object TransactionDomainModule { fun providePrepareForSendUseCase( transactionRepository: TransactionRepository, cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): PrepareForSendUseCase { - return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository) + return PrepareForSendUseCase( + transactionRepository = transactionRepository, + cardSdkConfigRepository = cardSdkConfigRepository, + getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) }, + ) } @Provides @@ -189,8 +194,13 @@ internal object TransactionDomainModule { fun provideSignUseCase( walletManagersFacade: WalletManagersFacade, cardSdkConfigRepository: CardSdkConfigRepository, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): SignUseCase { - return SignUseCase(cardSdkConfigRepository, walletManagersFacade) + return SignUseCase( + cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) }, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index e71a54fcdd..1d5b58407d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -87,9 +87,10 @@ internal class CardSettingsModel @Inject constructor( val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet $userWalletId not found") } + .requireColdWallet() cardSdkConfigRepository.isBiometricsRequestPolicy = - userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY] + userWallet.scanResponse.card.isAccessCodeSet && settingsRepository.shouldSaveAccessCodes() } } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 6b5c655b9e..643710e4c2 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -24,8 +24,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.common.LogConfig -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.models.ApplicationId @@ -212,15 +210,11 @@ internal class MainViewModel @Inject constructor( } private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService { - val cardProvider: () -> ScanResponse? = { - userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - } - return MoonPayService( apiKey = environmentConfig.moonPayApiKey, secretKey = environmentConfig.moonPayApiSecretKey, logEnabled = LogConfig.network.moonPayService, - cardProvider = { cardProvider.invoke()?.card }, + userWalletProvider = { userWalletsListManager.selectedUserWalletSync }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 15c1ace4f6..37690ee484 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -134,7 +134,7 @@ internal class WelcomeMiddleware { } private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) { - // TODO [REDACTED_TASK_KEY] + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics if (userWallet !is UserWallet.Cold) { return diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index f57f247e74..02bc4321ae 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -12,7 +12,7 @@ import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus @@ -27,7 +27,7 @@ class MoonPayService( private val apiKey: String, private val secretKey: String, private val logEnabled: Boolean, - private val cardProvider: () -> CardDTO?, + private val userWalletProvider: () -> UserWallet?, ) : ExchangeService { override val initializationStatus: StateFlow @@ -103,8 +103,8 @@ class MoonPayService( } override fun availableForSell(currency: Currency): Boolean { - val card = cardProvider() ?: return false - val checkCardExchange = !card.isStart2Coin + val userWallet = userWalletProvider() ?: return false + val checkCardExchange = userWallet !is UserWallet.Cold || !userWallet.scanResponse.card.isStart2Coin if (!checkCardExchange) return false diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 20cdc81e55..18929af1a4 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -197,11 +197,10 @@ internal class DefaultManageTokensRepository( ) private fun getSupportedBlockchains(userWallet: UserWallet?): List { - return (userWallet as? UserWallet.Cold)?.scanResponse?.let { - it.card.supportedBlockchains(it.cardTypesResolver, excludedBlockchains) // TODO [REDACTED_TASK_KEY] - } ?: Blockchain.entries.filter { - !it.isTestnet() && it !in excludedBlockchains - } + return userWallet?.supportedBlockchains(excludedBlockchains) + ?: Blockchain.entries.filter { + !it.isTestnet() && it !in excludedBlockchains + } } // endregion diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 585b0a640a..7ceb082dd5 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -17,12 +17,10 @@ import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections @@ -542,11 +540,10 @@ internal class DefaultNFTRepository @Inject constructor( } private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { - // TODO [REDACTED_TASK_KEY] - val scanResponse = userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().scanResponse + val userWallet = userWalletsStore.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(backendId) ?: return false return blockchain.canHandleNFTs() && - scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains) + userWallet.canHandleToken(blockchain, excludedBlockchains) } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 8cda8c8835..cfffffb159 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -22,7 +22,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency @@ -170,23 +169,17 @@ internal class DefaultHotCryptoRepository( // TODO: [REDACTED_JIRA] private fun UserWallet.canHandleHotCrypto(hotToken: HotCryptoResponse.Token): Boolean { - if (this !is UserWallet.Cold) { - return true // TODO [REDACTED_TASK_KEY] - } - val isToken = hotToken.contractAddress != null && hotToken.decimalCount != null val blockchain = hotToken.networkId?.let { Blockchain.fromNetworkId(it) } ?: return false return if (isToken) { - scanResponse.card.canHandleToken( + canHandleToken( blockchain = blockchain, - cardTypesResolver = cardTypesResolver, excludedBlockchains = excludedBlockchains, ) } else { - scanResponse.card.canHandleBlockchain( + canHandleBlockchain( blockchain = blockchain, - cardTypesResolver = cardTypesResolver, excludedBlockchains = excludedBlockchains, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt index a05b79077f..896fd35e4f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -18,7 +18,6 @@ import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -68,7 +67,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( } }, onError = { - handleFetchTokensError(error = it, userWallet = userWallet.requireColdWallet()) // TODO 11142 + handleFetchTokensError(error = it, userWallet = userWallet) }, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt index 9d73ca64a0..a959276e85 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -4,7 +4,7 @@ import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -35,7 +35,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr get() = emptySet() override fun produce(): Flow> { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId).requireColdWallet() // TODO [REDACTED_TASK_KEY] + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) if (!userWallet.isMultiCurrency) { error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index 5ea8f38be1..dffa80fd65 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -53,7 +53,7 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { private fun List.mapToNewDerivations(): List { val config = when (userWallet) { is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card) - is UserWallet.Hot -> Wallet2CardConfig // TODO create config [REDACTED_TASK_KEY] + is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet } return mapNotNull { network -> val blockchain = network.toBlockchain() diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index 9ae11b61c8..c7569b5a3b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -46,7 +46,8 @@ class HotWalletAccessor @Inject constructor( auth = auth, block = { blockAuth -> block(blockAuth).also { - // TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code + // if user has biometry enabled, we set it as the new auth method if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt index 32bc4a3ecc..a189ade273 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/extensions/CardSdk.kt @@ -100,6 +100,20 @@ fun UserWallet.canHandleToken(blockchain: Blockchain, excludedBlockchains: Exclu } } +fun UserWallet.canHandleBlockchain(blockchain: Blockchain, excludedBlockchains: ExcludedBlockchains): Boolean { + return when (this) { + is UserWallet.Cold -> { + scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + excludedBlockchains = excludedBlockchains, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + is UserWallet.Hot -> blockchain.isTestnet().not() && + blockchain !in excludedBlockchains + } +} + /** * The same as [CardDTO.supportedTokens] but with supportedTokens input, if previously calculated */ diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index 7d01fd1314..f1c80f3ba0 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -4,8 +4,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.card.common.extensions.supportedBlockchains -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -26,21 +24,13 @@ class FilterAvailableNetworksForWalletUseCase( it.walletId == userWalletId } ?: return networks.toSet() - return when (userWallet) { - is UserWallet.Cold -> { - val supportedBlockchains = userWallet.scanResponse.card.supportedBlockchains( - cardTypesResolver = userWallet.scanResponse.cardTypesResolver, - excludedBlockchains = excludedBlockchains, - ) + val supportedBlockchains = userWallet.supportedBlockchains( + excludedBlockchains = excludedBlockchains, + ) - networks.filter { - val blockchain = Blockchain.fromNetworkId(it.networkId) - supportedBlockchains.contains(blockchain) - }.toSet() - } - is UserWallet.Hot -> { - networks.toSet() // TODO [REDACTED_TASK_KEY] - } - } + return networks.filter { + val blockchain = Blockchain.fromNetworkId(it.networkId) + supportedBlockchains.contains(blockchain) + }.toSet() } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt index 36f6ad081d..a6f1353437 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt @@ -10,14 +10,14 @@ import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.models.TwinKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet class PrepareForSendUseCase( private val transactionRepository: TransactionRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, + private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( transactionData: TransactionData, @@ -56,7 +56,13 @@ class PrepareForSendUseCase( } private fun createSigner(userWallet: UserWallet): TransactionSigner { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + return when (userWallet) { + is UserWallet.Hot -> getHotTransactionSigner(userWallet) + is UserWallet.Cold -> getColdSigner(userWallet) + } + } + + private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner { val card = userWallet.scanResponse.card val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt index ec98b2d43c..07b302c99c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SignUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.blockchain.common.TransactionSigner import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins @@ -11,25 +12,21 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.network.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet class SignUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, + private val getHotTransactionSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( hash: ByteArray, userWallet: UserWallet, network: Network, ): Either { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - val card = userWallet.scanResponse.card - val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins - - val signer = cardSdkConfigRepository.getCommonSigner( - cardId = card.cardId.takeIf { isCardNotBackedUp }, - twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), - ) + val signer = when (userWallet) { + is UserWallet.Hot -> getHotTransactionSigner(userWallet) + is UserWallet.Cold -> getColdSigner(userWallet) + } val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) ?: error("WalletManager not found") @@ -39,4 +36,14 @@ class SignUseCase( is CompletionResult.Success -> signResult.data.right() } } + + private fun getColdSigner(userWallet: UserWallet.Cold): TransactionSigner { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + return cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index 8b8bda5d78..4707a09f81 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] add derivation config + val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt index 99348982ad..25d25bd977 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt @@ -13,7 +13,8 @@ fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Bo return when (this) { is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath) is UserWallet.Hot -> { - val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) // TODO [REDACTED_TASK_KEY]: handle hot wallet config + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet + val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) val list = if (blockchain == Blockchain.Cardano) { listOf( CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index a5b029fa67..e02206355e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -118,10 +118,14 @@ internal class DetailsModel @Inject constructor( modelScope.launch { val userWallets = getWalletsUseCase.invokeSync() - val scanResponse = - getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - ?: error("Selected wallet is null") + val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull() + ?: error("Selected wallet is null") + if (selectedUserWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Send feedback + } + + val scanResponse = selectedUserWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch val feedbackType = when { @@ -140,11 +144,13 @@ internal class DetailsModel @Inject constructor( } private fun openUseDesk() { - val scanResponse = - getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY] - ?: error("Selected wallet is null") + val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null") - val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] UseDesk + } + + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return router.push(AppRoute.Usedesk(cardInfo)) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt index 65fc1a9b0f..4de22cecd9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -215,7 +215,8 @@ internal class OnboardingVisaModel @Inject constructor( private fun tryToFindExistingWalletCardId(targetAddress: String): String? { val wallets = getWalletsUseCase.invokeSync().filter { it.isLocked.not() } - return wallets.filterIsInstance().firstOrNull { wallet -> // TODO [REDACTED_TASK_KEY] + // TODO [REDACTED_TASK_KEY] [Hot Wallet] Visa 1.0 flow. Hot wallet as a customer wallet + return wallets.filterIsInstance().firstOrNull { wallet -> wallet.scanResponse.card.wallets.any { val derivedKey = it.derivedKeys[VisaUtilities.visaDefaultDerivationPath] ?: return@any false diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index d889c54e08..890446c68e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -18,7 +18,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -57,7 +57,6 @@ internal class OnrampOperationModel @Inject constructor( private val selectedUserWallet = getWalletsUseCase.invokeSync() .first { it.walletId == params.userWalletId } - .requireColdWallet() init { analyticsEventHandler.send( @@ -154,7 +153,7 @@ internal class OnrampOperationModel @Inject constructor( } private fun showErrorIfDemoModeOrElse(action: () -> Unit) { - if (isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { + if (selectedUserWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { val alertUM = AlertDemoModeUM(onConfirmClick = {}) val message = DialogMessage( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index d2ce88c2d0..2cdae1912c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -15,7 +15,6 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetTokenListUseCase @@ -61,7 +60,6 @@ internal class OnrampTokenListModel @Inject constructor( private val params: OnrampTokenListComponent.Params = paramsContainer.require() private val userWallet by lazy { getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - .requireColdWallet() // TODO [REDACTED_TASK_KEY] } init { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 8dac9245d8..2296485619 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase @@ -325,8 +326,12 @@ internal class SendConfirmModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index d23c93d631..de07147e93 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -465,8 +465,12 @@ internal class SendModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index cdb172282b..734bc45601 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase @@ -231,8 +232,12 @@ internal class NFTSendConfirmModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 287366db87..0bebb6f107 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -206,8 +206,12 @@ internal class NFTSendModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return modelScope.launch { sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index d5efc4972b..31e0e9e58c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -859,6 +859,10 @@ internal class StakingModel @Inject constructor( modelScope.launch { val network = cryptoCurrencyStatus.currency.network + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) .getOrElse { error("CardInfo must be not null") } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 7327c593f4..9ea883b4e5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -96,8 +96,12 @@ internal class SwapAlertFactory @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + val cardInfo = - getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return // TODO [REDACTED_TASK_KEY] + getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return sendFeedbackEmailUseCase( type = FeedbackEmailType.SwapProblem( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index a748df6f52..2e8026f8db 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1351,8 +1351,6 @@ internal class SwapModel @Inject constructor( val transaction = dataState.swapDataModel?.transaction val fromCurrencyStatus = dataState.fromCryptoCurrency ?: initialFromStatus val network = fromCurrencyStatus.currency.network - val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY] - .getOrElse { error("CardInfo must be not null") } saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -1366,6 +1364,13 @@ internal class SwapModel @Inject constructor( ), ) + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse) + .getOrElse { error("CardInfo must be not null") } + val email = FeedbackEmailType.SwapProblem( cardInfo = cardInfo, providerName = dataState.selectedProvider?.name.orEmpty(), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index dcdfa6c203..42ea4e9a86 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -96,6 +96,7 @@ internal class WalletSettingsModel @Inject constructor( getWalletNotificationsEnabledUseCase(params.userWalletId), ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine + wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] [Hot Wallet] Wallet Settings val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() state.update { value -> value.copy( @@ -126,7 +127,7 @@ internal class WalletSettingsModel @Inject constructor( } private fun buildItems( - userWallet: UserWallet, + userWallet: UserWallet.Cold, dialogNavigation: SlotNavigation, isRenameWalletAvailable: Boolean, isNFTEnabled: Boolean, @@ -137,9 +138,8 @@ internal class WalletSettingsModel @Inject constructor( ): PersistentList = itemsBuilder.buildItems( userWalletId = userWallet.walletId, userWalletName = userWallet.name, - isReferralAvailable = userWallet !is UserWallet.Cold || userWallet.cardTypesResolver.isTangemWallet(), - isLinkMoreCardsAvailable = userWallet is UserWallet.Cold && - userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, + isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), + isLinkMoreCardsAvailable = userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, isManageTokensAvailable = userWallet.isMultiCurrency, isRenameWalletAvailable = isRenameWalletAvailable, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, @@ -162,7 +162,6 @@ internal class WalletSettingsModel @Inject constructor( messageSender.send(message) }, onLinkMoreCardsClick = { - userWallet.requireColdWallet() onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) }, onReferralClick = { onReferralClick(userWallet) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index b6c6a7f60b..fa98ada4f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -243,8 +243,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { neverToSuggestRateAppUseCase() - val scanResponse = - getSelectedUserWallet()?.requireColdWallet()?.scanResponse ?: return@launch // TODO [REDACTED_TASK_KEY] + val userWallet = getSelectedUserWallet() ?: return@launch + + if (userWallet is UserWallet.Hot) { + return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val scanResponse = userWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch sendFeedbackEmailUseCase(type = FeedbackEmailType.RateCanBeBetter(cardInfo = cardInfo)) @@ -285,7 +290,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onSupportClick() { - val scanResponse = getSelectedUserWallet()?.requireColdWallet()?.scanResponse ?: return // TODO [REDACTED_TASK_KEY] + val userWallet = getSelectedUserWallet() ?: return + + if (userWallet is UserWallet.Hot) { + return // TODO [REDACTED_TASK_KEY] [Hot Wallet] Email feedback flow + } + + val scanResponse = userWallet.requireColdWallet().scanResponse val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return modelScope.launch { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 5099b4e6b9..e717d41de5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.requireColdWallet 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.model.WalletCardState @@ -40,8 +39,8 @@ internal class UpdateWalletCardsCountTransformer( return when (this) { is WalletCardState.Content -> copy( additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), - imageResId = walletImageResolver.resolve(userWallet = userWallet.requireColdWallet()), // TODO [REDACTED_TASK_KEY] - cardCount = userWallet.requireColdWallet().getCardsCount(), // TODO [REDACTED_TASK_KEY] + imageResId = walletImageResolver.resolve(userWallet = userWallet), + cardCount = (userWallet as? UserWallet.Cold)?.getCardsCount(), ) else -> this } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 9307b4c066..a475345feb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -45,8 +45,8 @@ internal class WalletLoadingStateFactory( walletCardState = WalletCardState.Loading( id = userWallet.walletId, title = userWallet.name, - additionalInfo = null, // TODO [REDACTED_TASK_KEY] - imageResId = null, // TODO [REDACTED_TASK_KEY] + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + imageResId = null, dropDownItems = persistentListOf(), ), buttons = createMultiWalletActions(userWallet), From be160483d2af90b98829498bd2615b68520b8dd1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 13:54:22 +0400 Subject: [PATCH 031/165] Updated on 2026-08-14 --- .../di/domain/NotificationsDomainModule.kt | 35 +++- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + .../common/ui/notifications/NotificationId.kt | 11 ++ .../local/preferences/PreferencesKeys.kt | 2 + .../DefaultNotificationsRepository.kt | 61 +----- .../DefaultPushNotificationsRepository.kt | 68 +++++++ .../notifications/di/NotificationsModule.kt | 6 + .../DefaultNotificationsRepositoryTest.kt | 175 +++++------------ .../DefaultPushNotificationsRepositoryTest.kt | 178 ++++++++++++++++++ .../notifications/GetApplicationIdUseCase.kt | 12 +- ...etworksAvailableForNotificationsUseCase.kt | 6 +- .../notifications/SendPushTokenUseCase.kt | 6 +- .../SetShouldShowNotificationUseCase.kt | 12 ++ .../ShouldShowNotificationUseCase.kt | 12 ++ .../repository/NotificationsRepository.kt | 44 +++-- .../repository/PushNotificationsRepository.kt | 20 ++ .../GetApplicationIdUseCaseTest.kt | 60 +++--- .../notifications/SendPushTokenUseCaseTest.kt | 16 +- .../component/ChooseManagedTokensComponent.kt | 1 + features/manage-tokens/impl/build.gradle.kts | 1 + .../model/ChooseManagedTokensModel.kt | 22 ++- .../entrypoint/model/SendEntryPointModel.kt | 28 ++- features/swap-v2/impl/build.gradle.kts | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 8 + 25 files changed, 519 insertions(+), 268 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt create mode 100644 data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt create mode 100644 data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index c7b61fd1e0..b35ae23d95 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.* import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider @@ -18,20 +19,22 @@ internal object NotificationsDomainModule { @Provides @Singleton - fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase { + fun providesGetApplicationIdUseCase( + pushNotificationsRepository: PushNotificationsRepository, + ): GetApplicationIdUseCase { return GetApplicationIdUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, ) } @Provides @Singleton fun providesSendPushTokenUseCase( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -56,6 +59,26 @@ internal object NotificationsDomainModule { ) } + @Provides + @Singleton + fun providesShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): ShouldShowNotificationUseCase { + return ShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + + @Provides + @Singleton + fun providesSetShouldShowNotificationUseCase( + notificationsRepository: NotificationsRepository, + ): SetShouldShowNotificationUseCase { + return SetShouldShowNotificationUseCase( + notificationsRepository = notificationsRepository, + ) + } + @Provides @Singleton fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { @@ -65,8 +88,8 @@ internal object NotificationsDomainModule { @Provides @Singleton fun provideGetNetworksAvailableForNotifications( - notificationsRepository: NotificationsRepository, + pushNotificationsRepository: PushNotificationsRepository, ): GetNetworksAvailableForNotificationsUseCase { - return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository) + return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 9ecfe8e5c6..c18697013f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -434,6 +434,7 @@ internal class ChildFactory @Inject constructor( initialCurrency = route.initialCurrency, selectedCurrency = route.selectedCurrency, source = ChooseManagedTokensComponent.Source.valueOf(route.source.name), + showSendViaSwapNotification = route.showSendViaSwapNotification, ), componentFactory = chooseManagedTokensComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d1afb593f0..def139603e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -133,6 +133,7 @@ sealed class AppRoute(val path: String) : Route { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, ) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") { enum class Source { SendViaSwap, diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt new file mode 100644 index 0000000000..4b770200d0 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationId.kt @@ -0,0 +1,11 @@ +package com.tangem.common.ui.notifications + +/** + * NotificationId represents unique identifiers for notifications in the app. + * + * These ids can be used with [ShouldShowNotificationUseCase] and [SetShouldShowNotificationUseCase] + * to check or update the visibility state of notifications. + */ +enum class NotificationId(val key: String) { + SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"), +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 874a358083..d39a5c15a8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -137,6 +137,8 @@ object PreferencesKeys { val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } + + fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") // endregion // region Promo diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index d1f92e8262..dcb85f6ef2 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -1,48 +1,22 @@ package com.tangem.data.notifications -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody -import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.* -import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import javax.inject.Inject -internal class DefaultNotificationsRepository @Inject constructor( - private val tangemTechApi: TangemTechApi, - private val appInfoProvider: AppInfoProvider, +class DefaultNotificationsRepository @Inject constructor( private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, ) : NotificationsRepository { - override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = appInfoProvider.platform.lowercase(), - device = appInfoProvider.device, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - pushToken = pushToken, - ), - ).getOrThrow().appId.let(::ApplicationId) + override suspend fun shouldShowNotification(key: String): Boolean { + return appPreferencesStore.getSyncOrDefault(PreferencesKeys.getShouldShowNotificationKey(key), true) } - override suspend fun saveApplicationId(appId: ApplicationId) { - appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) - } - - override suspend fun getApplicationId(): ApplicationId? { - return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) - ?.let(::ApplicationId) + override suspend fun setShouldShowNotifications(key: String, value: Boolean) { + appPreferencesStore.store(PreferencesKeys.getShouldShowNotificationKey(key), value) } override suspend fun incrementTronTokenFeeNotificationShowCounter() { @@ -61,25 +35,4 @@ internal class DefaultNotificationsRepository @Inject constructor( default = 0, ) } - - override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { - withContext(dispatchers.io) { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = appInfoProvider.osVersion, - language = appInfoProvider.language, - timezone = appInfoProvider.timezone, - version = appInfoProvider.appVersion, - ), - ).getOrThrow() - } - } - - override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { - tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { - NotificationsEligibleNetworkConverter.convert(it) - } - } } \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt new file mode 100644 index 0000000000..b881aff0c5 --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -0,0 +1,68 @@ +package com.tangem.data.notifications + +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.repository.PushNotificationsRepository +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultPushNotificationsRepository @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val appInfoProvider: AppInfoProvider, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PushNotificationsRepository { + + override suspend fun createApplicationId(pushToken: String?): ApplicationId = withContext(dispatchers.io) { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = appInfoProvider.platform.lowercase(), + device = appInfoProvider.device, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + pushToken = pushToken, + ), + ).getOrThrow().appId.let(::ApplicationId) + } + + override suspend fun saveApplicationId(appId: ApplicationId) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId.value) + } + + override suspend fun getApplicationId(): ApplicationId? { + return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + ?.let(::ApplicationId) + } + + override suspend fun sendPushToken(appId: ApplicationId, pushToken: String) { + withContext(dispatchers.io) { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + version = appInfoProvider.appVersion, + ), + ).getOrThrow() + } + } + + override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { + tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().mapNotNull { + NotificationsEligibleNetworkConverter.convert(it) + } + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt index 555ac66802..18f26e5763 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.notifications.di import com.tangem.data.notifications.DefaultNotificationsRepository +import com.tangem.data.notifications.DefaultPushNotificationsRepository import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -12,6 +14,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface NotificationsModule { + @Binds + @Singleton + fun bindPushNotificationsRepository(repository: DefaultPushNotificationsRepository): PushNotificationsRepository + @Binds @Singleton fun bindNotificationsRepository(repository: DefaultNotificationsRepository): NotificationsRepository diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 13d4dbb6b1..a92861eca6 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -1,30 +1,21 @@ package com.tangem.data.notifications -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.stringPreferencesKey -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.utils.info.AppInfoProvider +import com.google.common.truth.Truth.assertThat import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.google.common.truth.Truth.assertThat -import com.squareup.moshi.Moshi -import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.tangemTech.models.* -import com.tangem.domain.notifications.models.ApplicationId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import androidx.datastore.preferences.core.Preferences +import com.squareup.moshi.Moshi +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.flowOf class DefaultNotificationsRepositoryTest { - private val tangemTechApi: TangemTechApi = mockk() - private val appInfoProvider: AppInfoProvider = mockk() private val preferencesDataStore: DataStore = mockk() private val appPreferencesStore = AppPreferencesStore( moshi = Moshi.Builder().build(), @@ -32,147 +23,77 @@ class DefaultNotificationsRepositoryTest { preferencesDataStore = preferencesDataStore, ) private val repository = DefaultNotificationsRepository( - tangemTechApi = tangemTechApi, - appInfoProvider = appInfoProvider, appPreferencesStore = appPreferencesStore, - dispatchers = TestingCoroutineDispatcherProvider(), ) @Test - fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + fun `GIVEN shouldShowNotification returns true WHEN called THEN returns true`() = runTest { // GIVEN - val pushToken = "test-push-token" - val expectedAppId = ApplicationId("test-app-id") - val expectedAppIdResponse = NotificationApplicationIdResponse( - appId = expectedAppId.value, - ) - coEvery { appInfoProvider.platform } returns "android" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( - expectedAppIdResponse, - ) + val key = "test-key" + val preferences = mockk(relaxed = true) + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns true + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.createApplicationId(pushToken) + val result = repository.shouldShowNotification(key) // THEN - assertThat(result).isEqualTo(expectedAppId) - coVerify { - tangemTechApi.createApplicationId( - NotificationApplicationCreateBody( - platform = "android", - device = "test-device", - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - pushToken = pushToken, - ), - ) - } + assertThat(result).isTrue() } @Test - fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + fun `GIVEN shouldShowNotification returns false WHEN called THEN returns false`() = runTest { // GIVEN - val appId = ApplicationId("test-app-id") + val key = "test-key" val preferences = mockk(relaxed = true) - coEvery { preferencesDataStore.updateData(any()) } returns preferences + every { preferences[PreferencesKeys.getShouldShowNotificationKey(key)] } returns false + coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - repository.saveApplicationId(appId) + val result = repository.shouldShowNotification(key) + + // THEN + assertThat(result).isFalse() + } + + @Test + fun `GIVEN setShouldShowNotifications WHEN called THEN stores value in preferences`() = runTest { + // GIVEN + val key = "test-key" + val value = false + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.setShouldShowNotifications(key, value) // THEN coVerify { preferencesDataStore.updateData(any()) } } @Test - fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + fun `GIVEN incrementTronTokenFeeNotificationShowCounter WHEN called THEN increments counter`() = runTest { // GIVEN - val expectedAppId = ApplicationId("test-app-id") + coEvery { preferencesDataStore.updateData(any()) } returns mockk(relaxed = true) + + // WHEN + repository.incrementTronTokenFeeNotificationShowCounter() + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN getTronTokenFeeNotificationShowCounter WHEN called THEN returns counter value`() = runTest { + // GIVEN + val expectedCount = 5 val preferences = mockk(relaxed = true) - val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) - every { preferences[key] } returns expectedAppId.value + every { preferences[PreferencesKeys.TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY] } returns expectedCount coEvery { preferencesDataStore.data } returns flowOf(preferences) // WHEN - val result = repository.getApplicationId() + val result = repository.getTronTokenFeeNotificationShowCounter() // THEN - assertThat(result).isEqualTo(expectedAppId) - } - - @Test - fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { - // GIVEN - val appId = ApplicationId("test-app-id") - val pushToken = "test-push-token" - coEvery { appInfoProvider.device } returns "test-device" - coEvery { appInfoProvider.osVersion } returns "11" - coEvery { appInfoProvider.language } returns "en" - coEvery { appInfoProvider.appVersion } returns "5.21.1" - coEvery { appInfoProvider.timezone } returns "UTC" - coEvery { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.sendPushToken(appId, pushToken) - - // THEN - coVerify { - tangemTechApi.updatePushTokenForApplicationId( - appId.value, - NotificationApplicationCreateBody( - pushToken = pushToken, - systemVersion = "11", - language = "en", - timezone = "UTC", - version = "5.21.1", - ), - ) - } - } - - @Test - fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { - // GIVEN - val expectedNetworks = listOf( - CryptoNetworkResponse( - id = 1, - name = "Ethereum", - networkId = "ethereum", - ), - CryptoNetworkResponse( - id = 2, - name = "Bitcoin", - networkId = "bitcoin", - ), - ) - coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( - expectedNetworks, - ) - - // WHEN - val result = repository.getEligibleNetworks() - - // THEN - assertThat(result).hasSize(2) - assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) - assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) - coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + assertThat(result).isEqualTo(expectedCount) } } \ No newline at end of file diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt new file mode 100644 index 0000000000..b423cb27ed --- /dev/null +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultPushNotificationsRepositoryTest.kt @@ -0,0 +1,178 @@ +package com.tangem.data.notifications + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPushNotificationsRepositoryTest { + private val tangemTechApi: TangemTechApi = mockk() + private val appInfoProvider: AppInfoProvider = mockk() + private val preferencesDataStore: DataStore = mockk() + private val appPreferencesStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private val repository = DefaultPushNotificationsRepository( + tangemTechApi = tangemTechApi, + appInfoProvider = appInfoProvider, + appPreferencesStore = appPreferencesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + // GIVEN + val pushToken = "test-push-token" + val expectedAppId = ApplicationId("test-app-id") + val expectedAppIdResponse = NotificationApplicationIdResponse( + appId = expectedAppId.value, + ) + coEvery { appInfoProvider.platform } returns "android" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( + expectedAppIdResponse, + ) + + // WHEN + val result = repository.createApplicationId(pushToken) + + // THEN + assertThat(result).isEqualTo(expectedAppId) + coVerify { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = "android", + device = "test-device", + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + pushToken = pushToken, + ), + ) + } + } + + @Test + fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + coEvery { preferencesDataStore.updateData(any()) } returns preferences + + // WHEN + repository.saveApplicationId(appId) + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + // GIVEN + val expectedAppId = ApplicationId("test-app-id") + val preferences = mockk(relaxed = true) + val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) + every { preferences[key] } returns expectedAppId.value + coEvery { preferencesDataStore.data } returns flowOf(preferences) + + // WHEN + val result = repository.getApplicationId() + + // THEN + assertThat(result).isEqualTo(expectedAppId) + } + + @Test + fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { + // GIVEN + val appId = ApplicationId("test-app-id") + val pushToken = "test-push-token" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.appVersion } returns "5.21.1" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.sendPushToken(appId, pushToken) + + // THEN + coVerify { + tangemTechApi.updatePushTokenForApplicationId( + appId.value, + NotificationApplicationCreateBody( + pushToken = pushToken, + systemVersion = "11", + language = "en", + timezone = "UTC", + version = "5.21.1", + ), + ) + } + } + + @Test + fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { + // GIVEN + val expectedNetworks = listOf( + CryptoNetworkResponse( + id = 1, + name = "Ethereum", + networkId = "ethereum", + ), + CryptoNetworkResponse( + id = 2, + name = "Bitcoin", + networkId = "bitcoin", + ), + ) + coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( + expectedNetworks, + ) + + // WHEN + val result = repository.getEligibleNetworks() + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) + assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) + coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt index 963d3b99d7..289328d75d 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -2,25 +2,25 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock class GetApplicationIdUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { private val mutex = Mutex() suspend operator fun invoke(): Either = Either.catch { - val localApplicationId = notificationsRepository.getApplicationId() + val localApplicationId = pushNotificationsRepository.getApplicationId() if (localApplicationId != null) return@catch localApplicationId mutex.withLock { - val doubleCheckedId = notificationsRepository.getApplicationId() + val doubleCheckedId = pushNotificationsRepository.getApplicationId() if (doubleCheckedId != null) return@withLock doubleCheckedId - val newApplicationId = notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + val newApplicationId = pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) newApplicationId } } diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt index edc3de5294..9264da1a16 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetNetworksAvailableForNotificationsUseCase.kt @@ -2,13 +2,13 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.NotificationsEligibleNetwork -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository class GetNetworksAvailableForNotificationsUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, ) { suspend operator fun invoke(): Either> = Either.catch { - notificationsRepository.getEligibleNetworks() + pushNotificationsRepository.getEligibleNetworks() } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt index 63247b9a7d..d33888f342 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt @@ -2,16 +2,16 @@ package com.tangem.domain.notifications import arrow.core.Either import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider class SendPushTokenUseCase( - private val notificationsRepository: NotificationsRepository, + private val pushNotificationsRepository: PushNotificationsRepository, private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, ) { suspend operator fun invoke(applicationId: ApplicationId): Either = Either.catch { val token = pushNotificationsTokenProvider.getToken() - notificationsRepository.sendPushToken(applicationId, token) + pushNotificationsRepository.sendPushToken(applicationId, token) } } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..44413fe106 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SetShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class SetShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String, value: Boolean) { + notificationsRepository.setShouldShowNotifications(key, value) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt new file mode 100644 index 0000000000..657ab7819d --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/ShouldShowNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.repository.NotificationsRepository + +class ShouldShowNotificationUseCase( + private val notificationsRepository: NotificationsRepository, +) { + + suspend operator fun invoke(key: String): Boolean { + return notificationsRepository.shouldShowNotification(key) + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index a8c46854df..377fc84984 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,24 +1,40 @@ package com.tangem.domain.notifications.repository -import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.models.NotificationsEligibleNetwork - +/** + * Repository interface for managing local notification logic and state. + * + * This interface provides methods to check and update whether specific notifications should be shown, + * as well as to track the display count for certain notifications (e.g., Tron token fee). + * + * Note: This repository is responsible only for the local logic and state (such as preferences and counters) + * regarding notifications. It does **not** directly show or hide notifications to the user. + * The actual display and hiding of notifications in the UI is handled by [NotificationsUM], + * which uses this repository to determine the appropriate behavior. + */ interface NotificationsRepository { - @Throws - suspend fun createApplicationId(pushToken: String? = null): ApplicationId + /** + * Checks whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @return true if the notification should be shown, false otherwise. + */ + suspend fun shouldShowNotification(key: String): Boolean - suspend fun saveApplicationId(appId: ApplicationId) - - suspend fun getApplicationId(): ApplicationId? + /** + * Sets whether a notification with the given [key] should be shown to the user. + * @param key The unique identifier for the notification. + * @param value true if the notification should be shown, false otherwise. + */ + suspend fun setShouldShowNotifications(key: String, value: Boolean) + /** + * Gets the number of times the Tron token fee notification has been shown. + * @return The current show counter for the Tron token fee notification. + */ suspend fun getTronTokenFeeNotificationShowCounter(): Int + /** + * Increments the counter tracking how many times the Tron token fee notification has been shown. + */ suspend fun incrementTronTokenFeeNotificationShowCounter() - - @Throws - suspend fun sendPushToken(appId: ApplicationId, pushToken: String) - - @Throws - suspend fun getEligibleNetworks(): List } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt new file mode 100644 index 0000000000..338a74b0ce --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.notifications.repository + +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork + +interface PushNotificationsRepository { + + @Throws + suspend fun createApplicationId(pushToken: String? = null): ApplicationId + + suspend fun saveApplicationId(appId: ApplicationId) + + suspend fun getApplicationId(): ApplicationId? + + @Throws + suspend fun sendPushToken(appId: ApplicationId, pushToken: String) + + @Throws + suspend fun getEligibleNetworks(): List +} \ No newline at end of file diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 6b2984554b..e3a191f205 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder @@ -15,14 +15,14 @@ import java.net.SocketTimeoutException class GetApplicationIdUseCaseTest { - private val notificationsRepository: NotificationsRepository = mockk() - private val useCase = GetApplicationIdUseCase(notificationsRepository) + private val pushNotificationsRepository: PushNotificationsRepository = mockk() + private val useCase = GetApplicationIdUseCase(pushNotificationsRepository) @Test fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { // GIVEN val expectedApplicationId = ApplicationId("test-app-id") - coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId + coEvery { pushNotificationsRepository.getApplicationId() } returns expectedApplicationId // WHEN val result = useCase() @@ -30,10 +30,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -41,9 +41,9 @@ class GetApplicationIdUseCaseTest { fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { // GIVEN val newApplicationId = ApplicationId("new-app-id") - coEvery { notificationsRepository.getApplicationId() } returns null - coEvery { notificationsRepository.createApplicationId() } returns newApplicationId - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.getApplicationId() } returns null + coEvery { pushNotificationsRepository.createApplicationId() } returns newApplicationId + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val result = useCase() @@ -52,10 +52,10 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) coVerifyOrder { - notificationsRepository.getApplicationId() - notificationsRepository.getApplicationId() - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(newApplicationId) + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.getApplicationId() + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(newApplicationId) } } @@ -63,7 +63,7 @@ class GetApplicationIdUseCaseTest { fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest { // GIVEN val expectedError = SocketTimeoutException("Test error") - coEvery { notificationsRepository.getApplicationId() } throws expectedError + coEvery { pushNotificationsRepository.getApplicationId() } throws expectedError // WHEN val result = useCase() @@ -71,10 +71,10 @@ class GetApplicationIdUseCaseTest { // THEN assertThat(result).isInstanceOf(Either.Left::class.java) assertThat((result as Either.Left).value).isEqualTo(expectedError) - coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.getApplicationId() } coVerify(inverse = true) { - notificationsRepository.createApplicationId() - notificationsRepository.saveApplicationId(any()) + pushNotificationsRepository.createApplicationId() + pushNotificationsRepository.saveApplicationId(any()) } } @@ -85,14 +85,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -108,9 +108,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } @Test @@ -119,14 +119,14 @@ class GetApplicationIdUseCaseTest { val newApplicationId = ApplicationId("new-app-id") var isIdCreated = false - coEvery { notificationsRepository.getApplicationId() } answers { + coEvery { pushNotificationsRepository.getApplicationId() } answers { if (!isIdCreated) null else newApplicationId } - coEvery { notificationsRepository.createApplicationId() } answers { + coEvery { pushNotificationsRepository.createApplicationId() } answers { isIdCreated = true newApplicationId } - coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + coEvery { pushNotificationsRepository.saveApplicationId(newApplicationId) } returns Unit // WHEN val results = coroutineScope { @@ -143,9 +143,9 @@ class GetApplicationIdUseCaseTest { assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isEqualTo(newApplicationId) } - coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } - coVerify(exactly = 1) { notificationsRepository.createApplicationId() } - coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + coVerify(exactly = PARALLEL_COUNT + 1) { pushNotificationsRepository.getApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.createApplicationId() } + coVerify(exactly = 1) { pushNotificationsRepository.saveApplicationId(newApplicationId) } } companion object { diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt index 18d716fdf0..fe84d79f16 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -3,7 +3,7 @@ package com.tangem.domain.notifications import arrow.core.Either import com.google.common.truth.Truth.assertThat import com.tangem.domain.notifications.models.ApplicationId -import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.utils.notifications.PushNotificationsTokenProvider import io.mockk.coEvery import io.mockk.coVerify @@ -14,16 +14,16 @@ import org.junit.Test class SendPushTokenUseCaseTest { - private lateinit var notificationsRepository: NotificationsRepository + private lateinit var pushNotificationsRepository: PushNotificationsRepository private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider private lateinit var sendPushTokenUseCase: SendPushTokenUseCase @Before fun setup() { - notificationsRepository = mockk() + pushNotificationsRepository = mockk() pushNotificationsTokenProvider = mockk() sendPushTokenUseCase = SendPushTokenUseCase( - notificationsRepository = notificationsRepository, + pushNotificationsRepository = pushNotificationsRepository, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -34,14 +34,14 @@ class SendPushTokenUseCaseTest { val applicationId = ApplicationId("test-app-id") val token = "test-token" coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } returns Unit // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Right(Unit)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } @Test @@ -51,13 +51,13 @@ class SendPushTokenUseCaseTest { val token = "test-token" val expectedError = RuntimeException("Network error") coEvery { pushNotificationsTokenProvider.getToken() } returns token - coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError + coEvery { pushNotificationsRepository.sendPushToken(applicationId, token) } throws expectedError // WHEN val result = sendPushTokenUseCase(applicationId) // THEN assertThat(result).isEqualTo(Either.Left(expectedError)) - coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + coVerify(exactly = 1) { pushNotificationsRepository.sendPushToken(applicationId, token) } } } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index a12efc7654..d17cd2b862 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -12,6 +12,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val source: Source, + val showSendViaSwapNotification: Boolean, ) enum class Source { diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index e28cec1763..c6a2a28283 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.swap.models) + implementation(projects.domain.notifications) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 83d65cc3a8..fe98829fd5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.managetokens.choosetoken.model import androidx.annotation.StringRes import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -15,6 +16,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.notifications.SetShouldShowNotificationUseCase import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM import com.tangem.features.managetokens.component.ChooseManagedTokensComponent @@ -43,6 +45,7 @@ internal class ChooseManagedTokensModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val uiMessageSender: UiMessageSender, + private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, paramsContainer: ParamsContainer, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { @@ -107,18 +110,21 @@ internal class ChooseManagedTokensModel @Inject constructor( } private fun getNotification(): NotificationUM? { - return when (params.source) { - Source.SendViaSwap -> ChooseManagedTokensNotificationUM.SendViaSwap( - onCloseClick = ::removeNotification, - ) + return if (params.source == Source.SendViaSwap && params.showSendViaSwapNotification) { + ChooseManagedTokensNotificationUM.SendViaSwap(onCloseClick = ::removeNotification) + } else { + null } } private fun removeNotification() { - uiState.update { - it.copy( - notificationUM = null, - ) + modelScope.launch { + setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false) + uiState.update { + it.copy( + notificationUM = null, + ) + } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 2f52fcdd3e..3bdbfcaf8d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -2,9 +2,11 @@ package com.tangem.features.send.v2.entrypoint.model import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger @@ -18,6 +20,7 @@ import jakarta.inject.Inject import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +@Suppress("LongParameterList") @ModelScoped internal class SendEntryPointModel @Inject constructor( paramsContainer: ParamsContainer, @@ -26,6 +29,7 @@ internal class SendEntryPointModel @Inject constructor( private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, ) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback { private val params: SendEntryPointComponent.Params = paramsContainer.require() @@ -36,15 +40,21 @@ internal class SendEntryPointModel @Inject constructor( private var swapChooseTokenListenerJobHolder = JobHolder() override fun onConvertToAnotherToken(lastAmount: String) { - appRouter.push( - AppRoute.ChooseManagedTokens( - userWalletId = params.userWalletId, - initialCurrency = params.cryptoCurrency, - selectedCurrency = null, - source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, - ), - ) - observeChooseSelectToken(lastAmount) + modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) + appRouter.push( + AppRoute.ChooseManagedTokens( + userWalletId = params.userWalletId, + initialCurrency = params.cryptoCurrency, + selectedCurrency = null, + source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + showSendViaSwapNotification = showSendViaSwapNotification, + ), + ) + observeChooseSelectToken(lastAmount) + } } override fun onCloseSwap(lastAmount: String) { diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 5cb929fe7e..ab1ccbbe75 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.txhistory.models) implementation(projects.domain.txhistory) + implementation(projects.domain.notifications) implementation(projects.domain.feedback.models) implementation(projects.domain.feedback) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 149ccd0de1..fa265d5e1c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,6 +23,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.models.SwapQuoteModel @@ -81,6 +83,7 @@ internal class SwapAmountModel @Inject constructor( private val swapAmountUpdateListener: SwapAmountUpdateListener, private val swapAmountReduceListener: SwapAmountReduceListener, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, + private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -221,6 +224,9 @@ internal class SwapAmountModel @Inject constructor( override fun onSelectTokenClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return modelScope.launch { + val showSendViaSwapNotification = shouldShowNotificationUseCase( + NotificationId.SendViaSwapTokenSelectorNotification.key, + ) val isEditMode = amountParams.currentRoute.firstOrNull()?.isEditMode == true val selectedCurrency = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus?.currency appRouter.push( @@ -229,6 +235,7 @@ internal class SwapAmountModel @Inject constructor( initialCurrency = primaryCryptoCurrency, selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + showSendViaSwapNotification = showSendViaSwapNotification, ), ) } @@ -259,6 +266,7 @@ internal class SwapAmountModel @Inject constructor( private fun confirmSendWithSwapClose() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data + val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (primaryCryptoCurrencyStatus != null) { From 158d625e050aa87c22d9a0d1c696afd3962d6ab3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 13:58:50 +0400 Subject: [PATCH 032/165] Updated on 2026-08-14 --- domain/account/.gitignore | 1 + domain/account/build.gradle.kts | 23 ++++ .../domain/account/models/AccountList.kt | 92 +++++++++++++++ .../account/models/AccountStatusList.kt | 21 ++++ .../domain/account/models/AccountListTest.kt | 108 ++++++++++++++++++ .../tangem/domain/models/account/Account.kt | 7 ++ .../tangem/domain/models/account/AccountId.kt | 2 + .../domain/models/account/AccountName.kt | 5 + .../domain/models/account/AccountStatus.kt | 28 +++++ settings.gradle.kts | 1 + 10 files changed, 288 insertions(+) create mode 100644 domain/account/.gitignore create mode 100644 domain/account/build.gradle.kts create mode 100644 domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt diff --git a/domain/account/.gitignore b/domain/account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts new file mode 100644 index 0000000000..d21a2a628e --- /dev/null +++ b/domain/account/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + + api(projects.domain.models) + api(projects.domain.wallets.models) + + implementation(deps.arrow.core) + implementation(deps.kotlin.serialization) + + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt new file mode 100644 index 0000000000..fdc9f393b0 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -0,0 +1,92 @@ +package com.tangem.domain.account.models + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import kotlinx.serialization.Serializable + +/** + * Represents a list of accounts associated with a user wallet + * + * @property userWallet the user wallet associated with the account list + * @property accounts a set of accounts belonging to the user wallet + * @property totalAccounts the total number of accounts + * +[REDACTED_AUTHOR] + */ +@Serializable +data class AccountList private constructor( + val userWallet: UserWallet, + val accounts: Set, + val totalAccounts: Int, +) { + + /** Retrieves the main crypto portfolio account from the list of accounts */ + val mainAccount: Account.CryptoPortfolio + get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + + /** + * Represents possible errors that can occur when creating an `AccountList` + */ + @Serializable + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "AccountListError" + + @Serializable + data object EmptyAccountsList : Error { + override fun toString(): String = "$tag: The accounts list cannot be empty" + } + + @Serializable + data object MainAccountNotFound : Error { + override fun toString(): String { + return "$tag: Account list does not contain a main crypto portfolio account" + } + } + + @Serializable + data object ExceedsMaxMainAccountsCount : Error { + override fun toString(): String { + return "$tag: There should be at most one main crypto portfolio in the account list" + } + } + } + + companion object { + + /** + * Factory method to create an `AccountList` instance. + * Validates the input to ensure the accounts list is not empty and contains exactly one main account. + * + * @param userWallet the user wallet associated with the account list + * @param accounts a set of accounts belonging to the user wallet + * @param totalAccounts the total number of accounts + */ + operator fun invoke( + userWallet: UserWallet, + accounts: Set, + totalAccounts: Int, + ): Either = either { + ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } + + val mainAccountsCount = accounts.mainAccountsCount() + ensure(mainAccountsCount == 1) { + if (mainAccountsCount == 0) { + Error.MainAccountNotFound + } else { + Error.ExceedsMaxMainAccountsCount + } + } + + AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) + } + + private fun Set.mainAccountsCount(): Int { + return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true } + } + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt new file mode 100644 index 0000000000..acd8f76d35 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.account.models + +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.wallet.UserWallet +import kotlinx.serialization.Serializable + +/** + * Represents a list of account statuses associated with a user wallet + * + * @property userWallet the user wallet to which the account statuses belong + * @property accountStatuses a set of account statuses associated with the user wallet + * @property totalAccounts the total number of accounts + * +[REDACTED_AUTHOR] + */ +@Serializable +data class AccountStatusList( + val userWallet: UserWallet, + val accountStatuses: Set, + val totalAccounts: Int, +) \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt new file mode 100644 index 0000000000..2abafd8934 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -0,0 +1,108 @@ +package com.tangem.domain.account.models + +import arrow.core.Either +import arrow.core.left +import com.google.common.truth.Truth +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountListTest { + + @Test + fun mainAccount() { + // Arrange + val mainAccount = createAccount(isMain = true) + + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(mainAccount), + totalAccounts = 1, + ) + .getOrNull()!! + + // Act + val actual = accountList.mainAccount + + // Assert + val expected = mainAccount + Truth.assertThat(actual).isEqualTo(expected) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Create { + + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(userWallet) + } + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: CreateTestModel) { + // Act + val actual = AccountList( + userWallet = userWallet, + accounts = model.accounts, + totalAccounts = model.accounts.size, + ) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + CreateTestModel( + accounts = emptySet(), + expected = AccountList.Error.EmptyAccountsList.left(), + ), + CreateTestModel( + accounts = setOf(createAccount(isMain = false)), + expected = AccountList.Error.MainAccountNotFound.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount(isMain = true), + createAccount(isMain = true), + ), + expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), + ), + createAccount(isMain = true).let { + CreateTestModel( + accounts = setOf(it), + expected = AccountList( + userWallet = userWallet, + accounts = setOf(it), + totalAccounts = 1, + ), + ) + }, + ) + } + + data class CreateTestModel( + val accounts: Set, + val expected: Either, + ) + + private fun createAccount(isMain: Boolean = false): Account.CryptoPortfolio { + return mockk { + every { isMainAccount } returns isMain + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index 398930b72e..bbea116526 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -8,12 +8,14 @@ import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable /** * Represents an account * [REDACTED_AUTHOR] */ +@Serializable sealed interface Account { /** Unique identifier of the account */ @@ -36,6 +38,7 @@ sealed interface Account { * @property isArchived indicates whether the account is archived * @property cryptoCurrencyList list of tokens associated with the account */ + @Serializable data class CryptoPortfolio private constructor( override val accountId: AccountId, override val name: AccountName, @@ -64,6 +67,7 @@ sealed interface Account { * @property sortType sorting type for the tokens * @property groupType grouping type for the tokens */ + @Serializable data class CryptoCurrencyList( val currencies: Set, val sortType: TokensSortType, @@ -73,14 +77,17 @@ sealed interface Account { /** * Represents possible errors when creating a crypto portfolio account */ + @Serializable sealed interface Error { /** Error indicating that the account name is blank */ + @Serializable data class AccountNameError(val cause: AccountName.Error) : Error { override fun toString(): String = cause.toString() } /** Error indicating that the derivation index is negative */ + @Serializable data object NegativeDerivationIndex : Error { override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative" } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index 2f5728579c..725f7143e9 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -1,6 +1,7 @@ package com.tangem.domain.models.account import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable /** * Represents a unique identifier for an account @@ -8,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId * @property value a unique string value that distinguishes this account * @property userWalletId the identifier of the user wallet associated with the account */ +@Serializable data class AccountId( val value: String, val userWalletId: UserWalletId, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt index e51a8e5a30..532687ef6c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -3,6 +3,7 @@ package com.tangem.domain.models.account import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import kotlinx.serialization.Serializable /** * Represents an account name @@ -11,6 +12,7 @@ import arrow.core.raise.ensure * [REDACTED_AUTHOR] */ +@Serializable data class AccountName private constructor( val value: String, ) { @@ -18,11 +20,13 @@ data class AccountName private constructor( /** * Represents possible validation errors */ + @Serializable sealed interface Error { /** * Error indicating that the account name is blank */ + @Serializable data object Empty : Error { override fun toString(): String = "${Empty::class.simpleName}: Account name cannot be blank" } @@ -30,6 +34,7 @@ data class AccountName private constructor( /** * Error indicating that the account name exceeds the maximum allowed length */ + @Serializable data object ExceedsMaxLength : Error { override fun toString(): String { return "${ExceedsMaxLength::class.simpleName}: Account name cannot exceed $MAX_LENGTH characters" diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt new file mode 100644 index 0000000000..73bc34e70e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.tokenlist.TokenList +import kotlinx.serialization.Serializable + +/** + * Represents the status of an account + * +[REDACTED_AUTHOR] + */ +@Serializable +sealed interface AccountStatus { + + /** The account associated with this status */ + val account: Account + + /** + * Represents the status of a crypto portfolio account + * + * @property account the crypto portfolio account + * @property tokenList the list of tokens associated with the account + */ + @Serializable + data class CryptoPortfolio( + override val account: Account.CryptoPortfolio, + val tokenList: TokenList, + ) : AccountStatus +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 9eb66f4052..de1aaf92ce 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -276,6 +276,7 @@ include(":features:welcome:impl") include(":domain:models") include(":domain:legacy") +include(":domain:account") include(":domain:card") include(":domain:core") include(":domain:demo") From 3b181b08c7a810627ce581c19644fea9c91484fd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 19:04:52 +0400 Subject: [PATCH 033/165] Updated on 2026-08-14 --- .../send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 24445e46cf..cbf1e1c4b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -362,7 +362,7 @@ private fun ExpandedCustomFeeItems( showDivider = false, modifier = Modifier .background( - color = TangemTheme.colors.background.action, + color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium, ), ) From 5103f81da803c6e7288679bc30094077d574979b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 17:18:57 +0200 Subject: [PATCH 034/165] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 1f665af06c..3aee2f0af5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,8 +80,8 @@ reownWeb3 = "1.1.2" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" chucker = "4.0.0" -mlKit-barcodeScanning = "17.2.0" -androidXCamera = "1.3.0" +mlKit-barcodeScanning = "17.3.0" +androidXCamera = "1.4.2" listenableFuture = "1.0" swipeRefreshLayout = "1.1.0" web3j = "4.12.3-SNAPSHOT" From 54558f1535cc5969e8d057162d6196cd3112024b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 15:44:41 +0000 Subject: [PATCH 035/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-446" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From 1713cfbf76ff6d95ce9e06ae044c7785907b263f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 16:57:13 +0000 Subject: [PATCH 036/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7ab02a4628..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1126" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-510" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From a52c9cb10687db4cdcde085e2728904d49e3e7e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 14:07:40 +0400 Subject: [PATCH 037/165] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/di/domain/AccountDomainModule.kt | 40 ++++++++++++++ .../usecase/AddCryptoPortfolioUseCase.kt | 55 +++++++++++++++++++ .../usecase/ArchiveCryptoPortfolioUseCase.kt | 22 ++++++++ .../usecase/RecoverCryptoPortfolioUseCase.kt | 25 +++++++++ .../usecase/UpdateCryptoPortfolioUseCase.kt | 50 +++++++++++++++++ 6 files changed, 193 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 19dd55b0e1..e90dbf8a85 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,6 +80,7 @@ configurations.androidTestImplementation { dependencies { implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.core) implementation(projects.domain.card) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt new file mode 100644 index 0000000000..22e627efca --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AccountDomainModule { + + @Provides + @Singleton + fun provideAddCryptoPortfolioUseCase(): AddCryptoPortfolioUseCase { + return AddCryptoPortfolioUseCase() + } + + @Provides + @Singleton + fun provideUpdateCryptoPortfolioUseCase(): UpdateCryptoPortfolioUseCase { + return UpdateCryptoPortfolioUseCase() + } + + @Provides + @Singleton + fun provideArchiveCryptoPortfolioUseCase(): ArchiveCryptoPortfolioUseCase { + return ArchiveCryptoPortfolioUseCase() + } + + @Provides + @Singleton + fun provideRecoverCryptoPortfolioUseCase(): RecoverCryptoPortfolioUseCase { + return RecoverCryptoPortfolioUseCase() + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..fb2ebb4717 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -0,0 +1,55 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase.Error.AccountCreation +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.wallet.UserWalletId +import java.util.UUID + +/** +[REDACTED_AUTHOR] + */ +class AddCryptoPortfolioUseCase { + + suspend operator fun invoke( + userWalletId: UserWalletId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: Int, + ): Either = either { + Account.CryptoPortfolio( + accountId = AccountId(userWalletId = userWalletId, value = UUID.randomUUID().toString()), + name = accountName.value, + accountIcon = icon, + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .mapLeft(::AccountCreation) + .bind() + + // TODO: [REDACTED_JIRA] + // Save to local store + // Save to backend (tokens migration) – asynchronously + } + + sealed interface Error { + + data class AccountCreation(val cause: Account.CryptoPortfolio.Error) : Error + + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error + + data object DataOperationFailed : Error + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..8e473b507d --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.models.account.AccountId + +/** +[REDACTED_AUTHOR] + */ +class ArchiveCryptoPortfolioUseCase { + + suspend operator fun invoke(accountId: AccountId): Either = either { + // TODO: [REDACTED_JIRA] + // Remove the account from the list of active accounts + // Add the account to the list of archived accounts + // Save to backend + } + + sealed interface Error { + data object DataOperationFailed : Error + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..2d27ad3a32 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId + +/** +[REDACTED_AUTHOR] + */ +class RecoverCryptoPortfolioUseCase { + + suspend operator fun invoke(accountId: AccountId): Either = either { + raise(Error.DataOperationFailed) + + // TODO: [REDACTED_JIRA] + // Remove the account from the list of archived accounts + // Add the account to the list of active accounts + // Save to backend + } + + sealed interface Error { + data object DataOperationFailed : Error + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt new file mode 100644 index 0000000000..cec8cbc039 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -0,0 +1,50 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import kotlin.random.Random + +/** +[REDACTED_AUTHOR] + */ +class UpdateCryptoPortfolioUseCase { + + suspend operator fun invoke( + accountId: AccountId, + name: AccountName? = null, + icon: CryptoPortfolioIcon? = null, + ): Either = either { + Account.CryptoPortfolio( + accountId = accountId, + name = name?.value ?: "Account", + accountIcon = icon ?: CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = Random.nextInt(from = 0, until = 21), + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .mapLeft { Error.DataOperationFailed } + .bind() + + // TODO: [REDACTED_JIRA] + // Create a domain model AccountName + // Get the current account by [accountId] + // Create a new domain model Account from old data considering new parameters + // Save information in local storage + // Save to backend + } + + sealed interface Error { + + data object DataOperationFailed : Error + } +} \ No newline at end of file From 4cac26e62838ddd71966468ee77d2440b8e5c663 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Jul 2025 16:31:37 +0500 Subject: [PATCH 038/165] Updated on 2026-08-14 --- .../swap/v2/impl/amount/model/SwapAmountModel.kt | 3 +++ .../transformers/SwapAmountSetQuotesTransformer.kt | 2 +- .../transformers/SwapQuoteLoadingStateTransformer.kt | 1 + .../swap/v2/impl/amount/ui/SwapAmountContent.kt | 10 +++++----- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index fa265d5e1c..efc0561da2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -51,6 +51,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler +import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.async @@ -481,6 +482,8 @@ internal class SwapAmountModel @Inject constructor( val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return + if (fromAmountValue.isZero()) return + val swapGroups = state.swapCurrencies.getGroupWithDirection(state.swapDirection) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 665a6a37fb..a3f6263d18 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -26,7 +26,7 @@ internal class SwapAmountSetQuotesTransformer( val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - val selectedQuote = if (isSilentReload) { + val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { prevState.selectedQuote } else { (bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt index a26deaf753..b431b0472a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteLoadingStateTransformer.kt @@ -16,6 +16,7 @@ internal object SwapQuoteLoadingStateTransformer : Transformer { } return prevState.copy( selectedQuote = SwapQuoteUM.Loading, + isPrimaryButtonEnabled = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 1ac34a19e2..199921a753 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -240,12 +240,12 @@ private fun SwapAmountInfo( SwapAmountInfoMain(amountFieldUM = amountFieldUM) SpacerWMax() AnimatedContent( - amountUM, - ) { wrappedAmountUM -> - if (wrappedAmountUM is SwapAmountUM.Content) { + amountFieldUM, + ) { wrappedFieldAmountUM -> + if (amountUM is SwapAmountUM.Content) { SwapAmountInfoExtra( - amountUM = wrappedAmountUM, - amountFieldUM = amountFieldUM, + amountUM = amountUM, + amountFieldUM = wrappedFieldAmountUM, onMaxAmountClick = onMaxAmountClick, onSelectTokenClick = onSelectTokenClick, ) From 8f69b1dce30d0b80216481c4718d32acdaf81645 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 31 Jul 2025 12:40:48 +0500 Subject: [PATCH 039/165] Updated on 2026-08-14 --- .../tangem/common/ui/footers/SendingText.kt | 4 +- .../java/com/tangem/utils/StringsSigns.kt | 1 + .../send/v2/api}/utils/ConfirmFooterUtils.kt | 8 +- ...endConfirmationNotificationsTransformer.kt | 4 +- ...dConfirmationNotificationsTransformerV2.kt | 4 +- ...endConfirmationNotificationsTransformer.kt | 4 +- .../swap/v2/impl/common/entity/ConfirmUM.kt | 13 ++- .../confirm/SendWithSwapConfirmComponent.kt | 3 + ...dWithSwapConfirmInitialStateTransformer.kt | 1 + ...wapConfirmationNotificationsTransformer.kt | 52 +++++++---- .../confirm/ui/SendWithSwapConfirmContent.kt | 90 +++++++++++++++++-- .../sendviaswap/ui/SendWithSwapContent.kt | 8 +- 12 files changed, 157 insertions(+), 35 deletions(-) rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/common => api/src/main/java/com/tangem/features/send/v2/api}/utils/ConfirmFooterUtils.kt (89%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt b/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt index 3f880fa882..b2c89117fe 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/footers/SendingText.kt @@ -49,10 +49,10 @@ fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { text = footerText.resolveAnnotatedReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, + color = TangemTheme.colors.text.tertiary, modifier = Modifier .fillMaxWidth() - .padding(12.dp), + .padding(16.dp), ) } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 534b602d2c..98ba454b64 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -9,6 +9,7 @@ object StringsSigns { const val LOWER_SIGN = "<" const val TILDE_SIGN = "~" const val COMA_SIGN = "," + const val POINT_SIGN = "." const val INFINITY_SIGN = "∞" const val NON_BREAKING_SPACE = '\u00A0' const val PERCENT = "%" diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt similarity index 89% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt index 3c13220638..4b0d95ede3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/utils/ConfirmFooterUtils.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/utils/ConfirmFooterUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.utils +package com.tangem.features.send.v2.api.utils import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee @@ -8,10 +8,10 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.api.R import com.tangem.utils.StringsSigns.COMA_SIGN -internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference { +fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: TextReference): TextReference { val suffix = when { fee.remainingEnergy == 0L -> { resourceReference( @@ -40,7 +40,7 @@ internal fun getTronTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSend return combinedReference(prefix, stringReference("$COMA_SIGN "), suffix) } -internal fun formatFooterFiatFee( +fun formatFooterFiatFee( amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index 906f76bb91..8e8987f2f8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -16,8 +16,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt index 7252942833..a5935435c3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -16,8 +16,8 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt index 561816abe5..fda1601aa9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -12,8 +12,8 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM -import com.tangem.features.send.v2.common.utils.formatFooterFiatFee -import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index 701181e121..b105bed8a1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -18,7 +18,18 @@ internal sealed class ConfirmUM { val showTapHelp: Boolean, val sendingFooter: TextReference, val notifications: ImmutableList, - ) : ConfirmUM() + val tosUM: TosUM?, + ) : ConfirmUM() { + data class TosUM( + val tosLink: LegalUM?, + val policyLink: LegalUM?, + ) + + data class LegalUM( + val title: TextReference, + val link: String, + ) + } data class Success( override val isPrimaryButtonEnabled: Boolean = true, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 47b374269f..d3cb5651c8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -41,6 +42,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( sendDestinationBlockComponent: SendDestinationBlockComponent.Factory, feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, sendNotificationsComponentFactory: SendNotificationsComponent.Factory, + private val urlOpener: UrlOpener, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: SendWithSwapConfirmModel = getOrCreateModel(params = params) @@ -151,6 +153,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( sendNotificationsUM = sendNotificationsUM, swapNotificationsComponent = swapNotificationsComponent, swapNotificationsUM = swapNotificationsUM, + onLinkClick = urlOpener::openUrl, modifier = modifier, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt index 4be5c0b56a..64e34d1673 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -15,6 +15,7 @@ internal class SendWithSwapConfirmInitialStateTransformer( showTapHelp = isShowTapHelp, sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), + tosUM = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt index 014e57650d..3176e70a6c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM 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.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -12,6 +13,8 @@ import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM @@ -27,6 +30,7 @@ internal class SendWithSwapConfirmationNotificationsTransformer : Transformer, swapNotificationsComponent: SwapNotificationsComponent, swapNotificationsUM: ImmutableList, + onLinkClick: (String) -> Unit, modifier: Modifier = Modifier, ) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Content Column(modifier = modifier) { LazyColumn( - modifier = Modifier.padding(horizontal = 12.dp), + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp), ) { item(key = "SendWithSwapBlocks") { Column( @@ -71,7 +78,80 @@ internal fun SendWithSwapConfirmContent( ) } } - SpacerHMax() - SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) + val sendFooter = confirmUM?.sendingFooter ?: TextReference.EMPTY + val legalFooter = getAnnotatedStringForLegals(confirmUM?.tosUM, onClick = onLinkClick) + SendingText( + footerText = if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { + combinedReference(sendFooter, legalFooter) + } else { + TextReference.EMPTY + }, + ) + } +} + +@Composable +private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick: (String) -> Unit): TextReference { + if (tosUM == null) return TextReference.EMPTY + val tos = tosUM.tosLink + val policy = tosUM.policyLink + return if (tos != null && policy != null) { + val tosTitle = tos.title.resolveReference() + val policyTitle = policy.title.resolveReference() + val fullString = stringResourceSafe(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle) + val tosIndex = fullString.indexOf(tosTitle) + val policyIndex = fullString.indexOf(policyTitle) + + annotatedReference { + append(StringsSigns.POINT_SIGN) + appendSpace() + append(fullString.substring(0, tosIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "TOS_TAG", + linkInteractionListener = { onClick(tos.link) }, + ), + block = { + appendColored( + text = fullString.substring(tosIndex, tosIndex + tosTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + append(fullString.substring(tosIndex + tosTitle.length, policyIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "POLICY_TAG", + linkInteractionListener = { onClick(policy.link) }, + ), + block = { + appendColored( + text = fullString.substring(policyIndex, policyIndex + policyTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } + } else { + val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" } + val legalTitle = legal.title.resolveReference() + val fullString = stringResourceSafe(id = R.string.express_legal_one_placeholder, legalTitle) + val legalIndex = fullString.indexOf(legalTitle) + + annotatedReference { + append(fullString.substring(0, legalIndex)) + withLink( + link = LinkAnnotation.Clickable( + tag = "LEGAL_TAG", + linkInteractionListener = { onClick(legal.link) }, + ), + block = { + appendColored( + text = fullString.substring(legalIndex, legalIndex + legalTitle.length), + color = TangemTheme.colors.text.accent, + ) + }, + ) + } } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index 17355d4aa4..e9d03433d4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -59,7 +59,13 @@ internal fun SendWithSwapContent( } // TODO refactor [REDACTED_TASK_KEY] val primaryButton = navigationUM.primaryButton - Row(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { TangemButton( modifier = Modifier.fillMaxWidth(), text = primaryButton.textReference.resolveReference(), From 5adb9506b21c0bf476651ad6050c3daa4c2c8ad5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 12:07:43 +0500 Subject: [PATCH 040/165] Updated on 2026-08-14 --- .../features/send/v2/send/DefaultSendComponent.kt | 7 ++++++- .../send/v2/send/confirm/model/SendConfirmModel.kt | 6 +++++- .../destination/model/SendDestinationModel.kt | 11 ++++++----- .../impl/sendviaswap/DefaultSendWithSwapComponent.kt | 7 ++++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 29b3573aa4..9cc15c97bb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -12,6 +12,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.ObserveLifecycleMode import com.arkivanov.decompose.value.subscribe import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child @@ -125,7 +126,11 @@ internal class DefaultSendComponent @AssistedInject constructor( val stackState by childStack.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = ::onChildBack) + BackHandler( + onBack = { + (state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack() + }, + ) SendContent( navigationUM = state.navigationUM, stackState = stackState, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 2296485619..4f8e762e40 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -594,7 +594,11 @@ internal class SendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, primaryButton = primaryButtonUM(), prevButton = null, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 6fdf7e3b09..db3ba80d27 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -145,6 +145,11 @@ internal class SendDestinationModel @Inject constructor( ) } + fun saveResult() { + val params = params as? SendDestinationComponentParams.DestinationParams ?: return + params.callback.onDestinationResult(uiState.value) + } + private fun initSenderAddress() { modelScope.launch { senderAddresses.value = getNetworkAddressesUseCase.invokeSync( @@ -288,11 +293,6 @@ internal class SendDestinationModel @Inject constructor( } } - private fun saveResult() { - val params = params as? SendDestinationComponentParams.DestinationParams ?: return - params.callback.onDestinationResult(uiState.value) - } - @Suppress("LongMethod") private fun configDestinationNavigation() { val params = params as? SendDestinationComponentParams.DestinationParams ?: return @@ -321,6 +321,7 @@ internal class SendDestinationModel @Inject constructor( isValid = state.isPrimaryButtonEnabled, ), ) + saveResult() } params.callback.onBackClick() }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 986c936162..e331657e56 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -11,6 +11,7 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.ObserveLifecycleMode import com.arkivanov.decompose.value.subscribe +import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -101,7 +102,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( val stackState by childStack.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = ::onChildBack) + BackHandler( + onBack = { + (state.navigationUM as? NavigationUM.Content)?.backIconClick() ?: onChildBack() + }, + ) SendWithSwapContent(navigationUM = state.navigationUM, stackState = stackState) } From 816d8532de35e5860832f2460c5c301a1469ee86 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Aug 2025 16:19:01 +0500 Subject: [PATCH 041/165] Updated on 2026-08-14 --- .../data/express/DefaultExpressRepository.kt | 6 ++- .../data/express/di/ExpressDataModule.kt | 3 ++ .../DefaultSingleQuoteStatusProducer.kt | 7 +-- .../data/swap/DefaultSwapRepositoryV2.kt | 51 ++++++++++--------- .../ui/SwapChooseTokenNetworkContent.kt | 16 ++++-- 5 files changed, 49 insertions(+), 34 deletions(-) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index 107cf20e9f..7fecd6950c 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -10,19 +10,21 @@ import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.filterIf import timber.log.Timber internal class DefaultExpressRepository( private val tangemExpressApi: TangemExpressApi, private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, ) : ExpressRepository { override suspend fun getProviders( userWallet: UserWallet, filterProviderTypes: List, - ): List { - return safeApiCall( + ): List = with(dispatchers.io) { + safeApiCall( call = { tangemExpressApi.getProviders( userWalletId = userWallet.walletId.stringValue, diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt index 75bfe50bf5..299b631162 100644 --- a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressErrorResolver import com.tangem.domain.express.ExpressRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,10 +35,12 @@ internal object ExpressDataModule { fun provideExpressRepository( tangemExpressApi: TangemExpressApi, appPreferencesStore: AppPreferencesStore, + dispatchers: CoroutineDispatcherProvider, ): ExpressRepository { return DefaultExpressRepository( tangemExpressApi = tangemExpressApi, appPreferencesStore = appPreferencesStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt index 692d5cb24a..4adc0ed55a 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt @@ -7,10 +7,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.* /** * Default implementation of [SingleQuoteStatusProducer] @@ -28,7 +25,7 @@ internal class DefaultSingleQuoteStatusProducer @AssistedInject constructor( override fun produce(): Flow { return quotesStatusesStore.get() - .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } + .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } ?: fallback } .distinctUntilChanged() .flowOn(dispatchers.default) } diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 46346745a5..e2d1ac8603 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -24,6 +24,7 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusProducer @@ -65,7 +66,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, - ): List = withContext(coroutineDispatcher.io) { + ): List = withContext(coroutineDispatcher.default) { val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } val allPairs = getPairsInternal( @@ -113,7 +114,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, - ): List = withContext(coroutineDispatcher.io) { + ): List = withContext(coroutineDispatcher.default) { val allPairs = getPairsInternal( userWallet = userWallet, initialCurrency = initialCurrency, @@ -330,25 +331,27 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet: UserWallet, from: List, to: List, - ) = safeApiCall( - call = { - tangemExpressApi.getPairs( - userWalletId = userWallet.walletId.stringValue, - refCode = ExpressUtils.getRefCode( - userWallet = userWallet, - appPreferencesStore = appPreferencesStore, - ), - body = PairsRequestBody( - from = tokenInfoConverter.convertList(from), - to = tokenInfoConverter.convertList(to), - ), - ).getOrThrow() - }, - onError = { - Timber.w(it, "Unable to get pairs") - throw it - }, - ) + ) = withContext(coroutineDispatcher.io) { + safeApiCall( + call = { + tangemExpressApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( + from = tokenInfoConverter.convertList(from), + to = tokenInfoConverter.convertList(to), + ), + ).getOrThrow() + }, + onError = { + Timber.w(it, "Unable to get pairs") + throw it + }, + ) + } /** * Send with swap specific currency status creation @@ -360,9 +363,9 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val quote = singleQuoteStatusSupplier.getSyncOrNull( params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId), - )?.right() + ) - if (quote == null) { + if (quote == null || quote.value is QuoteStatus.Empty) { singleQuoteStatusFetcher.invoke( params = SingleQuoteStatusFetcher.Params( rawCurrencyId = rawCurrencyId, @@ -373,7 +376,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( return currencyStatusProxyCreator.createCurrencyStatus( currency = cryptoCurrency, - maybeQuoteStatus = quote ?: singleQuoteStatusSupplier.getSyncOrNull( + maybeQuoteStatus = quote?.right() ?: singleQuoteStatusSupplier.getSyncOrNull( params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawCurrencyId), ).right(), maybeNetworkStatus = NetworkStatus( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index 03d5b2f8c9..d2c33cddad 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -24,12 +24,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -94,13 +95,22 @@ internal fun SwapChooseTokenNetworkContent(state: SwapChooseTokenNetworkContentU @Composable private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList) { - Column { - swapNetworks.fastForEach { network -> + Column( + modifier = Modifier + .padding(top = 8.dp) + .padding(16.dp), + ) { + swapNetworks.fastForEachIndexed { index, network -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier .fillMaxWidth() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = swapNetworks.lastIndex, + addDefaultPadding = false, + ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), From 4427ccf4b09d785a4b0024e9fe75c080a356ba8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 4 Aug 2025 12:19:28 +0400 Subject: [PATCH 042/165] Updated on 2026-08-14 --- .../send/v2/send/confirm/ui/SendConfirmContent.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 21e776b4ad..73b2a5a0fa 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility 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.foundation.lazy.LazyListScope @@ -13,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -49,9 +49,13 @@ internal fun SendConfirmContent( ) { val confirmUM = sendUM.confirmUM as? ConfirmUM.Content - Column { + Column( + modifier = Modifier.fillMaxSize(), + ) { LazyColumn( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens.spacing16), ) { blocks( uiState = sendUM, @@ -74,7 +78,6 @@ internal fun SendConfirmContent( ) } } - SpacerHMax() SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) } } From a0ba231dbdac654bdeefaf690550dd761c9bb11d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Aug 2025 13:25:26 +0000 Subject: [PATCH 043/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-446" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From 539c1a268976453e03c1b5a3b115003c3baa34d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Aug 2025 21:44:05 +0500 Subject: [PATCH 044/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 12 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + .../hotwallet/WalletActivationComponent.kt | 14 ++ .../entry/AddExistingWalletModel.kt | 49 ++++-- .../DefaultAddExistingWalletComponent.kt | 52 +++---- .../routing/AddExistingWalletChildFactory.kt | 3 +- .../CreateMobileWalletModel.kt | 2 +- .../check/ManualBackupCheckComponent.kt | 4 +- .../check/di/ManualBackupCheckModule.kt | 20 +++ .../check/entity/ManualBackupCheckUM.kt | 2 + .../check/model/ManualBackupCheckModel.kt | 73 +++++++-- .../phrase/ManualBackupPhraseComponent.kt | 2 + .../phrase/di/ManualBackupPhraseModule.kt | 20 +++ .../phrase/model/ManualBackupPhraseModel.kt | 42 ++++- .../start/di/ManualBackupStartModule.kt | 20 +++ .../stepper/api/HotWalletStepperComponent.kt | 13 +- .../entry/DefaultWalletActivationComponent.kt | 92 +++++++++++ .../entry/WalletActivationModel.kt | 147 ++++++++++++++++++ .../WalletActivationStepperStateManager.kt | 91 +++++++++++ .../entry/di/WalletActivationModule.kt | 42 +++++ .../routing/WalletActivationChildFactory.kt | 85 ++++++++++ .../entry/routing/WalletActivationRoute.kt | 31 ++++ .../entry/ui/WalletActivationContent.kt | 42 +++++ .../api/PushNotificationsParams.kt | 3 + .../impl/model/PushNotificationsModel.kt | 7 +- .../intents/WalletWarningsClickIntents.kt | 3 +- .../domain/GetMultiWalletWarningsFactory.kt | 3 +- 27 files changed, 808 insertions(+), 71 deletions(-) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 436c6fcc3a..0f2952af12 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -14,6 +14,7 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -91,6 +92,7 @@ internal class ChildFactory @Inject constructor( private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, + private val walletActivationComponentFactory: WalletActivationComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -385,6 +387,7 @@ internal class ChildFactory @Inject constructor( context = context, params = PushNotificationsParams( modelCallbacks = PushNotificationsModelCallbacksStub(), + nextRoute = AppRoute.Home(), ), componentFactory = pushNotificationsComponentFactory, ) @@ -464,6 +467,15 @@ internal class ChildFactory @Inject constructor( componentFactory = addExistingWalletComponentFactory, ) } + is AppRoute.WalletActivation -> { + createComponentChild( + context = context, + params = WalletActivationComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = walletActivationComponentFactory, + ) + } is AppRoute.SendEntryPoint -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index def139603e..f36983b866 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -301,6 +301,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable object AddExistingWallet : AppRoute(path = "/add_existing_wallet") + @Serializable + data class WalletActivation( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") + @Serializable data class SendEntryPoint( val userWalletId: UserWalletId, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt new file mode 100644 index 0000000000..8b99c19f36 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/WalletActivationComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface WalletActivationComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index b404d14746..ca924d1eb8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -13,8 +13,11 @@ import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletSt import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,30 +28,30 @@ internal class AddExistingWalletModel @Inject constructor( private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, ) : Model() { + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks() val accessCodeModelCallbacks = AccessCodeModelCallbacks() + val pushNotificationsCallbacks = PushNotificationsCallbacks() val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks() val stackNavigation = StackNavigation() + val startRoute = AddExistingWalletRoute.Start + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) - fun onChildBack(currentRoute: AddExistingWalletRoute) { - when (currentRoute) { + fun onChildBack() { + when (currentRoute.value) { + is AddExistingWalletRoute.Start -> router.pop() is AddExistingWalletRoute.Import -> stackNavigation.pop() is AddExistingWalletRoute.BackupCompleted -> Unit is AddExistingWalletRoute.SetAccessCode -> Unit is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop() is AddExistingWalletRoute.PushNotifications -> Unit is AddExistingWalletRoute.SetupFinished -> Unit - is AddExistingWalletRoute.Start -> Unit } } - fun onSkipAccessCode() { - navigateToPushNotificationsOrNext() - } - private fun navigateToPushNotificationsOrNext() { modelScope.launch { val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) @@ -62,6 +65,20 @@ internal class AddExistingWalletModel @Inject constructor( } } + private fun navigateToSetupFinished() { + stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished) + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onChildBack() + } + + override fun onSkipClick() { + navigateToPushNotificationsOrNext() + } + } + inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks { override fun onBackClick() { router.pop() @@ -74,13 +91,13 @@ internal class AddExistingWalletModel @Inject constructor( inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks { override fun onWalletImported(userWalletId: UserWalletId) { - stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted(userWalletId)) + stackNavigation.replaceAll(AddExistingWalletRoute.BackupCompleted(userWalletId)) } } inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { override fun onContinueClick(userWalletId: UserWalletId) { - stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode(userWalletId)) + stackNavigation.replaceAll(AddExistingWalletRoute.SetAccessCode(userWalletId)) } } @@ -94,6 +111,20 @@ internal class AddExistingWalletModel @Inject constructor( } } + inner class PushNotificationsCallbacks : PushNotificationsModelCallbacks { + override fun onAllowSystemPermission() { + navigateToSetupFinished() + } + + override fun onDenySystemPermission() { + navigateToSetupFinished() + } + + override fun onDismiss() { + navigateToSetupFinished() + } + } + inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { override fun onContinueClick() { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt index 53e611282b..e1e673e034 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/DefaultAddExistingWalletComponent.kt @@ -6,19 +6,20 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.extensions.TextReference import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletChildFactory -import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute import com.tangem.features.hotwallet.addexistingwallet.entry.ui.AddExistingWalletContent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch internal class DefaultAddExistingWalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @@ -30,13 +31,11 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( private val model: AddExistingWalletModel = getOrCreateModel(params) - private val startRoute = AddExistingWalletRoute.Start - private val innerStack = childStack( key = "addExistingWalletInnerStack", source = model.stackNavigation, serializer = null, - initialConfiguration = startRoute, + initialConfiguration = model.startRoute, handleBackButton = true, childFactory = { configuration, factoryContext -> addExistingWalletChildFactory.createChild( @@ -50,32 +49,28 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( private val stepperComponent = stepperComponentFactory.create( context = this, params = HotWalletStepperComponent.Params( - initState = HotWalletStepperComponent.StepperUM( - currentStep = 0, - steps = 0, - title = TextReference.EMPTY, - showBackButton = false, - showSkipButton = false, - showFeedbackButton = false, - ), - callback = object : HotWalletStepperComponent.ModelCallback { - override fun onBackClick() { - onChildBack() - } - - override fun onSkipClick() { - model.onSkipAccessCode() - } - }, + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, ), ) + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + @Composable override fun Content(modifier: Modifier) { val stackState by innerStack.subscribeAsState() val currentRoute = stackState.active.configuration - BackHandler(onBack = ::onChildBack) + BackHandler(onBack = model::onChildBack) val stepperState = stepperStateManager.getStepperState(currentRoute) stepperState?.let { stepperComponent.updateState(it) } @@ -86,17 +81,6 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor( ) } - private fun onChildBack() { - val isEmptyStack = innerStack.value.backStack.isEmpty() - - if (isEmptyStack) { - router.pop() - } else { - val currentRoute = innerStack.value.active.configuration - model.onChildBack(currentRoute) - } - } - @AssistedFactory interface Factory : AddExistingWalletComponent.Factory { override fun create(context: AppComponentContext, params: Unit): DefaultAddExistingWalletComponent diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 710b2da90f..8a25cc1176 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -9,7 +9,6 @@ import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupComplete import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent -import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub import com.tangem.features.pushnotifications.api.PushNotificationsParams import javax.inject.Inject @@ -63,7 +62,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create( context = childContext, params = PushNotificationsParams( - modelCallbacks = PushNotificationsModelCallbacksStub(), + modelCallbacks = model.pushNotificationsCallbacks, ), ) is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index a4a7a4f243..d59cf2e39f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -48,7 +48,7 @@ internal class CreateMobileWalletModel @Inject constructor( saveUserWalletUseCase( hotUserWalletBuilder.build(), ) - router.push(AppRoute.Wallet) + router.replaceAll(AppRoute.Wallet) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt index dfaf6fe787..7f25916f2d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/ManualBackupCheckComponent.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent import dagger.assisted.Assisted @@ -33,7 +33,7 @@ internal class ManualBackupCheckComponent @AssistedInject constructor( } data class Params( - val generatedWords: Mnemonic, + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt new file mode 100644 index 0000000000..34c8d3bb8c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/di/ManualBackupCheckModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.check.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupCheckModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupCheckModel::class) + fun bindManualBackupCheckModel(model: ManualBackupCheckModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt index 19eac1ac68..5d2bc48d20 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/entity/ManualBackupCheckUM.kt @@ -2,10 +2,12 @@ package com.tangem.features.hotwallet.manualbackup.check.entity import androidx.compose.ui.text.input.TextFieldValue import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf internal data class ManualBackupCheckUM( val onCompleteButtonClick: () -> Unit, val wordFields: ImmutableList, + val words: ImmutableList = persistentListOf(), val completeButtonEnabled: Boolean, val completeButtonProgress: Boolean, ) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt index ffe2a0ad15..6d62bc73c9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -2,16 +2,25 @@ package com.tangem.features.hotwallet.manualbackup.check.model import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.UpdateWalletUseCase import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.Boolean import kotlin.Int @@ -20,13 +29,15 @@ import kotlin.Suppress import kotlin.collections.List import kotlin.collections.all import kotlin.collections.map -import kotlin.error @Stable @ModelScoped internal class ManualBackupCheckModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val updateWalletUseCase: UpdateWalletUseCase, + private val tangemHotSdk: TangemHotSdk, ) : Model() { private val params = paramsContainer.require() @@ -35,6 +46,28 @@ internal class ManualBackupCheckModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow(getInitialUIState()) + init { + modelScope.launch { + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val seedPhrasePrivateInfo = tangemHotSdk.exportMnemonic(unlockHotWallet) + uiState.update { + it.copy( + words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> + WORD_FIELD_INDICES.contains(index + 1) + }.toImmutableList(), + ) + } + } + }.onFailure { + Timber.e(it) + } + } + } + @Suppress("MagicNumber") private fun getInitialUIState(): ManualBackupCheckUM { val wordFields = List(WORD_FIELD_INDICES.size) { index -> @@ -57,12 +90,37 @@ internal class ManualBackupCheckModel @Inject constructor( onCompleteButtonClick = { val currentUIState = uiState.value if (currentUIState.completeButtonEnabled) { - callbacks.onCompleteClick() + backupWallet() } }, ) } + private fun backupWallet() { + modelScope.launch { + uiState.update { + it.copy(completeButtonProgress = true) + } + + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + updateWalletUseCase(userWallet.walletId) { + userWallet.copy(backedUp = true) + } + callbacks.onCompleteClick() + } + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(completeButtonProgress = false) + } + } + } + } + private fun updateWordField(shownIndex: Int, newText: TextFieldValue) { uiState.update { currentState -> val updatedFields = currentState.wordFields.map { wordField -> @@ -89,14 +147,9 @@ internal class ManualBackupCheckModel @Inject constructor( } private fun checkWordField(word: String, shownIndex: Int): Boolean { - val generatedWords = params.generatedWords - val wordList = generatedWords.mnemonicComponents - - return if (shownIndex <= wordList.size) { - wordList[shownIndex - 1] == word - } else { - false - } + val words = uiState.value.words + val listIndex = WORD_FIELD_INDICES.indexOf(shownIndex) + return words.getOrNull(listIndex)?.let { word == it } == true } companion object { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt index 1dd8b360fc..62d7b79ed4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent import dagger.assisted.Assisted @@ -32,6 +33,7 @@ internal class ManualBackupPhraseComponent @AssistedInject constructor( } data class Params( + val userWalletId: UserWalletId, val callbacks: ModelCallbacks, ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt new file mode 100644 index 0000000000..3b5c8c8018 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/di/ManualBackupPhraseModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.phrase.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupPhraseModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupPhraseModel::class) + fun bindManualBackupPhraseModel(model: ManualBackupPhraseModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt index 4320a86026..e9625260ad 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -1,14 +1,24 @@ package com.tangem.features.hotwallet.manualbackup.phrase.model import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.UnlockHotWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Stable @@ -16,17 +26,39 @@ import javax.inject.Inject internal class ManualBackupPhraseModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val tangemHotSdk: TangemHotSdk, ) : Model() { private val params = paramsContainer.require() private val callbacks = params.callbacks internal val uiState: StateFlow - field = MutableStateFlow(getInitialUIState()) - - private fun getInitialUIState(): ManualBackupPhraseUM { - return ManualBackupPhraseUM( + field = MutableStateFlow( + ManualBackupPhraseUM( onContinueClick = callbacks::onContinueClick, - ) + ), + ) + + init { + modelScope.launch { + runCatching { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrElse { error("User wallet with id ${params.userWalletId} not found") } + if (userWallet is UserWallet.Hot) { + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + val seedPhrasePrivateInfo = tangemHotSdk.exportMnemonic(unlockHotWallet) + uiState.update { + it.copy( + words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> + ManualBackupPhraseUM.MnemonicGridItem(index + 1, s) + }.toImmutableList(), + ) + } + } + }.onFailure { + Timber.e(it) + } + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt new file mode 100644 index 0000000000..2f506e7b30 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/di/ManualBackupStartModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.hotwallet.manualbackup.start.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManualBackupStartModule { + + @Binds + @IntoMap + @ClassKey(ManualBackupStartModel::class) + fun bindManualBackupStartModel(model: ManualBackupStartModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt index c14de91c72..169d037699 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/stepper/api/HotWalletStepperComponent.kt @@ -15,7 +15,18 @@ interface HotWalletStepperComponent : ComposableContentComponent { val showBackButton: Boolean, val showSkipButton: Boolean, val showFeedbackButton: Boolean, - ) + ) { + companion object { + fun initialState() = StepperUM( + currentStep = 0, + steps = 0, + title = TextReference.EMPTY, + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } interface ModelCallback { fun onBackClick() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt new file mode 100644 index 0000000000..f24a1ea128 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/DefaultWalletActivationComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationChildFactory +import com.tangem.features.hotwallet.walletactivation.entry.ui.WalletActivationContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultWalletActivationComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: WalletActivationComponent.Params, + private val stepperStateManager: WalletActivationStepperStateManager, + walletActivationChildFactory: WalletActivationChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : WalletActivationComponent, AppComponentContext by appComponentContext { + + private val model: WalletActivationModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "walletActivationInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + walletActivationChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onChildBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + WalletActivationContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : WalletActivationComponent.Factory { + override fun create( + context: AppComponentContext, + params: WalletActivationComponent.Params, + ): DefaultWalletActivationComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt new file mode 100644 index 0000000000..3d03a96a04 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -0,0 +1,147 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.arkivanov.decompose.router.stack.replaceAll +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.settings.ShouldAskPermissionUseCase +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class WalletActivationModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + val accessCodeModelCallbacks = AccessCodeModelCallbacks() + val pushNotificationsCallbacks = PushNotificationsCallbacks() + val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = WalletActivationRoute.ManualBackupStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onChildBack() { + when (currentRoute.value) { + is WalletActivationRoute.ManualBackupStart -> router.pop() + is WalletActivationRoute.ManualBackupPhrase -> stackNavigation.pop() + is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop() + is WalletActivationRoute.ManualBackupCompleted -> Unit + is WalletActivationRoute.SetAccessCode -> Unit + is WalletActivationRoute.ConfirmAccessCode -> Unit + is WalletActivationRoute.PushNotifications -> Unit + is WalletActivationRoute.SetupFinished -> Unit + } + } + + private fun navigateToPushNotificationsOrNext() { + modelScope.launch { + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (shouldRequestPush) { + // is yet blocked by [REDACTED_TASK_KEY] + // stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications) + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } else { + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } + } + } + + private fun navigateToSetupFinished() { + stackNavigation.replaceAll(WalletActivationRoute.SetupFinished) + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onChildBack() + } + + override fun onSkipClick() { + navigateToPushNotificationsOrNext() + } + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + stackNavigation.push(WalletActivationRoute.ManualBackupPhrase) + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + stackNavigation.push( + WalletActivationRoute.ManualBackupCheck, + ) + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + stackNavigation.push(WalletActivationRoute.ManualBackupCompleted) + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + stackNavigation.push(WalletActivationRoute.SetAccessCode) + } + } + + inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks { + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode)) + } + + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + navigateToPushNotificationsOrNext() + } + } + + inner class PushNotificationsCallbacks : PushNotificationsModelCallbacks { + override fun onAllowSystemPermission() { + navigateToSetupFinished() + } + + override fun onDenySystemPermission() { + navigateToSetupFinished() + } + + override fun onDismiss() { + navigateToSetupFinished() + } + } + + inner class MobileWalletSetupFinishedModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks { + override fun onContinueClick() { + router.pop() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt new file mode 100644 index 0000000000..5a33b36b87 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationStepperStateManager.kt @@ -0,0 +1,91 @@ +package com.tangem.features.hotwallet.walletactivation.entry + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute +import javax.inject.Inject + +internal class WalletActivationStepperStateManager @Inject constructor() { + + fun getStepperState(route: WalletActivationRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is WalletActivationRoute.ManualBackupStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_CHECK, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_BACKUP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = false, + showSkipButton = true, + showFeedbackButton = false, + ) + is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_ACCESS_CODE, + steps = STEPS_COUNT, + title = resourceReference(R.string.access_code_navtitle), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_NOTIFICATIONS, + steps = STEPS_COUNT, + title = resourceReference(R.string.onboarding_title_notifications), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_DONE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 7 + + private const val STEP_BACKUP = 1 + private const val STEP_BACKUP_PHRASE = 2 + private const val STEP_BACKUP_CHECK = 3 + private const val STEP_BACKUP_COMPLETED = 4 + private const val STEP_ACCESS_CODE = 5 + private const val STEP_NOTIFICATIONS = 6 + private const val STEP_DONE = 7 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt new file mode 100644 index 0000000000..51362cff09 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/di/WalletActivationModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.walletactivation.entry.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.walletactivation.entry.DefaultWalletActivationComponent +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationStepperStateManager +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletActivationModuleBinds { + + @Binds + @Singleton + fun bindWalletActivationComponentFactory( + impl: DefaultWalletActivationComponent.Factory, + ): WalletActivationComponent.Factory + + @Binds + @IntoMap + @ClassKey(WalletActivationModel::class) + fun bindWalletActivationModel(model: WalletActivationModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object WalletActivationModule { + + @Provides + @Singleton + fun provideWalletActivationStepperStateManager(): WalletActivationStepperStateManager { + return WalletActivationStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt new file mode 100644 index 0000000000..2b93835929 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt @@ -0,0 +1,85 @@ +package com.tangem.features.hotwallet.walletactivation.entry.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent +import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel +import com.tangem.features.pushnotifications.api.PushNotificationsComponent +import com.tangem.features.pushnotifications.api.PushNotificationsParams +import javax.inject.Inject + +internal class WalletActivationChildFactory @Inject constructor( + private val pushNotificationsComponent: PushNotificationsComponent.Factory, + private val accessCodeComponentFactory: AccessCodeComponent.Factory, +) { + + fun createChild( + route: WalletActivationRoute, + childContext: AppComponentContext, + model: WalletActivationModel, + ): ComposableContentComponent { + return when (route) { + is WalletActivationRoute.ManualBackupStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupCheck -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + is WalletActivationRoute.ManualBackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + is WalletActivationRoute.SetAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = false, + userWalletId = model.params.userWalletId, + callbacks = model.accessCodeModelCallbacks, + ), + ) + is WalletActivationRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = true, + accessCodeToConfirm = route.accessCode, + userWalletId = model.params.userWalletId, + callbacks = model.accessCodeModelCallbacks, + ), + ) + is WalletActivationRoute.PushNotifications -> pushNotificationsComponent.create( + context = childContext, + params = PushNotificationsParams( + modelCallbacks = model.pushNotificationsCallbacks, + ), + ) + is WalletActivationRoute.SetupFinished -> MobileWalletSetupFinishedComponent( + context = childContext, + params = MobileWalletSetupFinishedComponent.Params( + callbacks = model.mobileWalletSetupFinishedModelCallbacks, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt new file mode 100644 index 0000000000..26b6998a2e --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationRoute.kt @@ -0,0 +1,31 @@ +package com.tangem.features.hotwallet.walletactivation.entry.routing + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +internal sealed class WalletActivationRoute : Route { + + @Serializable + object ManualBackupStart : WalletActivationRoute() + + @Serializable + object ManualBackupPhrase : WalletActivationRoute() + + @Serializable + data object ManualBackupCheck : WalletActivationRoute() + + @Serializable + object ManualBackupCompleted : WalletActivationRoute() + + @Serializable + data object SetAccessCode : WalletActivationRoute() + + @Serializable + data class ConfirmAccessCode(val accessCode: String) : WalletActivationRoute() + + @Serializable + object PushNotifications : WalletActivationRoute() + + @Serializable + object SetupFinished : WalletActivationRoute() +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt new file mode 100644 index 0000000000..0c2e2719e9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/ui/WalletActivationContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.walletactivation.entry.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute + +@Composable +internal fun WalletActivationContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt index 27bc4d7131..4e152b9c09 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt @@ -1,6 +1,9 @@ package com.tangem.features.pushnotifications.api +import com.tangem.common.routing.AppRoute + data class PushNotificationsParams( val isBottomSheet: Boolean = false, + val nextRoute: AppRoute? = null, val modelCallbacks: PushNotificationsModelCallbacks, ) \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index 735932ce2c..9a6115aa48 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -1,7 +1,6 @@ package com.tangem.features.pushnotifications.impl.model import androidx.compose.runtime.Stable -import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -71,7 +70,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onDenySystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home()) + params.nextRoute?.let { appRouter.push(it) } } } } @@ -85,7 +84,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onAllowSystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home()) + params.nextRoute?.let { appRouter.push(it) } } } } @@ -99,7 +98,7 @@ internal class PushNotificationsModel @Inject constructor( neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) params.modelCallbacks.onDenySystemPermission() if (!params.isBottomSheet) { - appRouter.push(AppRoute.Home()) + params.nextRoute?.let { appRouter.push(it) } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index fa98ada4f2..5e3757b744 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -387,7 +387,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } override fun onFinishWalletActivationClick() { - // TODO implement wallet activation process + val userWallet = getSelectedUserWallet() ?: return + appRouter.push(AppRoute.WalletActivation(userWallet.walletId)) } private suspend fun fetchCryptoCurrencies(userWalletId: UserWalletId, currencies: List) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 4427d9de7e..248f54a3e6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -266,8 +266,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) { if (userWallet !is UserWallet.Hot) return - // TODO [REDACTED_TASK_KEY] set an actual value - val shouldShowFinishActivation = false + val shouldShowFinishActivation = !userWallet.backedUp addIf( element = WalletNotification.FinishWalletActivation( From 8ad45b2fe8e4e54088d835e84291f267c99d599a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 08:21:14 +0000 Subject: [PATCH 045/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7ab02a4628..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1126" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-510" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From fd3d95097fe1456c510bb0da703109a0a9695eed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Aug 2025 15:45:35 +0400 Subject: [PATCH 046/165] Updated on 2026-08-14 --- data/account/.gitignore | 1 + data/account/build.gradle.kts | 37 ++++++++++++++ .../data/account/di/AccountDataModule.kt | 25 ++++++++++ .../DefaultAccountsCRUDRepository.kt | 47 ++++++++++++++++++ .../repository/AccountsCRUDRepository.kt | 49 +++++++++++++++++++ settings.gradle.kts | 1 + 6 files changed, 160 insertions(+) create mode 100644 data/account/.gitignore create mode 100644 data/account/build.gradle.kts create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt create mode 100644 domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt diff --git a/data/account/.gitignore b/data/account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts new file mode 100644 index 0000000000..464ba8486e --- /dev/null +++ b/data/account/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.account" +} + +dependencies { + + // region Project - Core + api(projects.core.utils) + // endregion + + // region Project - Domain + api(projects.domain.account) + api(projects.domain.models) + // endregion + + // Project - Data + implementation(projects.core.datasource) + // endregion + + // region DI + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Other Dependencies + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.timber) + // endregion +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt new file mode 100644 index 0000000000..e05c402f68 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.account.di + +import com.tangem.data.account.repository.DefaultAccountsCRUDRepository +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.repository.AccountsCRUDRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AccountDataModule { + + @Provides + @Singleton + fun provideAccountsCRUDRepository(userWalletsStore: UserWalletsStore): AccountsCRUDRepository { + return DefaultAccountsCRUDRepository( + runtimeStore = RuntimeSharedStore(), + userWalletsStore = userWalletsStore, + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt new file mode 100644 index 0000000000..76d86bd0fe --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -0,0 +1,47 @@ +package com.tangem.data.account.repository + +import arrow.core.Option +import arrow.core.Option.Companion.catch +import arrow.core.none +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.extensions.addOrReplace + +/** +[REDACTED_AUTHOR] + */ +// TODO: [REDACTED_JIRA] +internal class DefaultAccountsCRUDRepository( + private val runtimeStore: RuntimeSharedStore>, + private val userWalletsStore: UserWalletsStore, +) : AccountsCRUDRepository { + + override suspend fun getAccounts(userWalletId: UserWalletId): Option = catch { + runtimeStore.getSyncOrNull() + ?.firstOrNull { it.userWallet.walletId == userWalletId } + ?: return none() + } + + override suspend fun getAccount(accountId: AccountId): Option = catch { + runtimeStore.getSyncOrNull().orEmpty() + .flatMap { it.accounts } + .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + ?: return none() + } + + override suspend fun saveAccounts(accountList: AccountList) { + runtimeStore.update(emptyList()) { + it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } + } + } + + override fun getUserWallet(userWalletId: UserWalletId): UserWallet { + return userWalletsStore.getSyncStrict(userWalletId) + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt new file mode 100644 index 0000000000..70b8543734 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.account.repository + +import arrow.core.Option +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Repository interface for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +interface AccountsCRUDRepository { + + /** + * Retrieves a list of accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing the [AccountList] if found, or `Option.None` if not + */ + suspend fun getAccounts(userWalletId: UserWalletId): Option + + /** + * Retrieves a specific account by its unique identifier + * + * @param accountId the unique identifier of the account + * @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not + */ + suspend fun getAccount(accountId: AccountId): Option + + /** + * Saves a list of accounts to the repository + * + * @param accountList the list of accounts to be saved. + */ + @Throws + suspend fun saveAccounts(accountList: AccountList) + + /** + * Retrieves a user wallet by its unique identifier + * + * @param userWalletId the unique identifier of the user wallet + * @return the [UserWallet] associated with the given identifier + */ + @Throws + fun getUserWallet(userWalletId: UserWalletId): UserWallet +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index de1aaf92ce..bab69533de 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -334,6 +334,7 @@ include(":domain:wallet-manager:models") // endregion Domain modules // region Data modules +include(":data:account") include(":data:app-currency") include(":data:app-theme") include(":data:balance-hiding") From f6fad0e6d18011e7f5b573f8978381b29094cbd5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 17:09:04 +0700 Subject: [PATCH 047/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + core/res/src/main/res/values-de/strings.xml | 2 + core/res/src/main/res/values-es/strings.xml | 2 + core/res/src/main/res/values-fr/strings.xml | 2 + core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 21 ++ core/res/src/main/res/values-ru/strings.xml | 2 + .../src/main/res/values-uk-rUA/strings.xml | 2 + .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 19 + .../ui/components/fields/AmountTextField.kt | 27 +- .../ui/components/fields/AutoSizeTextField.kt | 204 +++++++++++ .../ui/components/fields/SimpleTextField.kt | 11 +- features/account/api/.gitignore | 1 + features/account/api/build.gradle.kts | 16 + .../account/AccountCreateEditComponent.kt | 14 + features/account/impl/.gitignore | 1 + features/account/impl/build.gradle.kts | 59 +++ .../createedit/AccountCreateEditModel.kt | 54 +++ .../DefaultAccountCreateEditComponent.kt | 38 ++ .../createedit/di/AccountCreateEditModule.kt | 27 ++ .../createedit/entity/AccountCreateEditUM.kt | 42 +++ .../createedit/ui/AccountCreateEditContent.kt | 344 ++++++++++++++++++ settings.gradle.kts | 3 + 24 files changed, 870 insertions(+), 25 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt create mode 100644 features/account/api/.gitignore create mode 100644 features/account/api/build.gradle.kts create mode 100644 features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt create mode 100644 features/account/impl/.gitignore create mode 100644 features/account/impl/build.gradle.kts create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e90dbf8a85..0f40005c3c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -235,6 +235,8 @@ dependencies { implementation(projects.features.createWalletSelection.impl) implementation(projects.features.home.api) implementation(projects.features.home.impl) + implementation(projects.features.account.api) + implementation(projects.features.account.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index ead5525ab6..47e0920719 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -120,6 +120,7 @@ Token hinzufügen Alle Erlauben + Betrag Analysen Anwenden Genehmigung @@ -165,6 +166,7 @@ Deaktiviert Trennen Erledigt + Bearbeiten Aktivieren Aktiviert Fehler diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1ce6e28079..39ee2f7881 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -109,6 +109,7 @@ Agregar token Todos Autorizar + Montante Analítica Aplicar Aprobación @@ -153,6 +154,7 @@ Desactivado Desconectar Listo + Editar Activar Activado Error diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index b28e3174c2..3973a3df94 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -99,6 +99,7 @@ Ajouter un jeton Tous Permettre + Montant Analytique Appliquer Approbation @@ -140,6 +141,7 @@ Désactivé Se déconnecter Exécuté + Modifier Activer Activé Erreur diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 7132029cfe..5caec91961 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -5,6 +5,7 @@ Questa carta non è progettata per funzionare con Tangem L\'importo inviato e il cambio non può essere inferiore a 1 ADA Accetta + Importo Saldo: %s Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index af6731c66e..93c2b842f3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,18 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アカウントを追加 + 保存 + アカウント名 + アカウント + 新しいアカウント + アカウントを追加 + アカウントを編集 + 編集を続ける + 破棄 + 新しいアカウントを破棄してもよろしいですか? + 編集内容を破棄してもよろしいですか? + 保存されていない変更 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 @@ -127,6 +139,7 @@ トークンを追加 すべて 許可する + 金額 アナリティクス 適用する 承認 @@ -170,6 +183,7 @@ 無効 切断 完了 + 編集 有効にする 有効 エラー @@ -1110,6 +1124,7 @@ 手数料見積りエラーです。サポートにフィードバックをお送りください。 スワップする 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 + 価格への影響が甚大です 残高不足 許可を与える スワップ @@ -1285,6 +1300,7 @@ このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 アクセスコードの設定 / 変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 + 現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。 取引通知 ウォレット設定 Tangem @@ -1426,15 +1442,19 @@ すべてのdAppを接続解除する ウォレットの変更の予測 取引をシミュレーションできませんでした。注意して続行してください。 + %sでは見積もりはサポートされていません + %sによる提案 悪意のある取引 このウォレットのポートフォリオに%sネットワークを追加します ウォレットに必要なネットワークはありません 新しい接続 ウォレットを別のdAppに接続する セッションなし + ウォレットの変更は検出されませんでした このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。 既知のセキュリティリスク リクエスト元 + とにかく送金 署名タイプ 宛先 取引リクエスト @@ -1443,6 +1463,7 @@ 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています ウォレットコネクト + 不審な取引 破棄 バックアップが中断されました。再開しますか? はい、再開します diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5a430645a7..474f997c12 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -96,6 +96,7 @@ Добавить токен Все Разрешить + Сумма Аналитика Применить Одобрение @@ -140,6 +141,7 @@ Отключено Отключить Готово + Изменить Включить Включено Ошибка diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 9b3e3dfe52..5a09461ddc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -96,6 +96,7 @@ Додати токен Усе Дозволити + Сума Аналітика Застосовувати Затвердження @@ -139,6 +140,7 @@ Вимкнуто Від\'єднати Готово + Редагувати Увімкнути Увімкнено Помилка diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index d5ccd296e0..b33588b325 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -40,6 +40,7 @@ 卡片設置 發送金額和找零不能少於1個ADA 接受 + 數量 允許 注意 餘額: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 704c48f319..70f17192aa 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,7 +12,18 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + Add account + Save Account name + Account + New account + Add account + Edit account + Keep Editing + Discard + Are you sure you want to discard new account? + Are you sure you want to discard edits? + Unsaved Changes Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. Sell @@ -132,6 +143,7 @@ Add token All Allow + Amount Analytics Apply Approval @@ -177,6 +189,7 @@ Disabled Disconnect Done + Edit Enable Enabled Error @@ -432,6 +445,8 @@ Finish Now Finish Wallet Activation To complete setup, secure app access with Access Code. + If you do, you\'ll need to start over. + Are you sure you want to exit the activation process? Recover an existing wallet stored in your Google Drive backup Google Drive Backup Go to backup @@ -1129,6 +1144,7 @@ Fee estimation error. Please send feedback to support. You swap Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + High price impact Insufficient funds Give Permission Swap @@ -1495,6 +1511,8 @@ Disconect All dApps Estimated wallet changes The transaction couldn\'t be simulated. Please proceed with caution. + Estimation is not supported for %s + Suggested by %s Malicious transaction Add the %s network to your portfolio for this wallet The wallet has no required networks @@ -1505,6 +1523,7 @@ This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets Known security risk Request from + Send anyway Signature Type To Transaction request diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index be5c0a9f2c..e4d65b95dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,12 +12,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphIntrinsics import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -77,24 +72,10 @@ fun AmountTextField( ) { val decimalFormat = rememberDecimalFormat() BoxWithConstraints(modifier = modifier) { - var fontSize = textStyle.fontSize - if (isAutoResize) { - val calculateIntrinsics = @Composable { - val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text - ParagraphIntrinsics( - text = transformedText, - style = textStyle.copy(fontSize = fontSize), - density = LocalDensity.current, - fontFamilyResolver = createFontFamilyResolver(LocalContext.current), - ) - } - var intrinsics = calculateIntrinsics() - with(LocalDensity.current) { - while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { - fontSize *= reduceFactor - intrinsics = calculateIntrinsics() - } - } + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize } val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color SimpleTextField( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt new file mode 100644 index 0000000000..23a4cf2348 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AutoSizeTextField.kt @@ -0,0 +1,204 @@ +package com.tangem.core.ui.components.fields + +import android.annotation.SuppressLint +import androidx.annotation.FloatRange +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphIntrinsics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.TextUnit +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Simple text field for auto size input. + * Can display aligned placeholder. + * + * @param value initial text + * @param onValueChange callback + * @param isAutoResize is text font auto resize + * @param reduceFactor font resize factor + * @param textStyle text and placeholder styles + * @param textFieldModifier modifier for [SimpleTextField] + * @param boxModifier modifier for [BoxWithConstraints] + * @see [SimpleTextField] for other text field params + */ +@SuppressLint("UnusedBoxWithConstraintsScope") +@Composable +fun AutoSizeTextField( + value: String, + onValueChange: (String) -> Unit, + + // region AutoSize + isAutoResize: Boolean = true, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, + + // region TextField + textFieldModifier: Modifier = Modifier, + boxModifier: Modifier = Modifier, + placeholder: TextReference? = null, + singleLine: Boolean = isAutoResize, + centered: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + color: Color = TangemTheme.colors.text.primary1, + textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), + placeholderColor: Color = TangemTheme.colors.text.disabled, + readOnly: Boolean = false, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + isValuePasted: Boolean = false, + onValuePastedTriggerDismiss: () -> Unit = {}, + decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null, +) { + BoxWithConstraints(modifier = boxModifier) { + val fontSize = if (isAutoResize) { + resizeFont(visualTransformation, value, textStyle, reduceFactor) + } else { + textStyle.fontSize + } + val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color + SimpleTextField( + value = value, + onValueChange = onValueChange, + textStyle = textStyle.copy( + fontSize = fontSize, + textDirection = TextDirection.ContentOrLtr, + ), + isValuePasted = isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + color = textColor, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + placeholder = placeholder, + placeholderColor = placeholderColor, + singleLine = singleLine, + interactionSource = interactionSource, + readOnly = readOnly, + centered = centered, + visualTransformation = visualTransformation, + decorationBox = decorationBox, + modifier = textFieldModifier, + ) + } +} + +@Composable +internal fun BoxWithConstraintsScope.resizeFont( + visualTransformation: VisualTransformation, + value: String, + textStyle: TextStyle, + reduceFactor: Double, +): TextUnit { + var result = textStyle.fontSize + val calculateIntrinsics = @Composable { + val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text + ParagraphIntrinsics( + text = transformedText, + style = textStyle.copy(fontSize = result), + density = LocalDensity.current, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + ) + } + var intrinsics = calculateIntrinsics() + with(LocalDensity.current) { + while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { + result *= reduceFactor + intrinsics = calculateIntrinsics() + } + } + return result +} + +// region preview +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun AmountTextFieldPreview( + @PreviewParameter(AutoSizeTextFieldPreviewProvider::class) data: AutoSizeTextFieldPreviewData, +) { + var text by remember { mutableStateOf(data.value) } + TangemThemePreview { + AutoSizeTextField( + textFieldModifier = Modifier.fillMaxWidth(), + value = text, + onValueChange = { text = it }, + centered = data.centered, + isAutoResize = data.isAutoResize, + placeholder = data.placeholder, + ) + } +} + +private class AutoSizeTextFieldPreviewProvider : PreviewParameterProvider { + override val values = sequenceOf( + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = true, + centered = false, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + AutoSizeTextFieldPreviewData( + value = "", + placeholder = stringReference("Placeholder"), + isAutoResize = false, + centered = true, + ), + ) +} + +private data class AutoSizeTextFieldPreviewData( + val value: String, + val placeholder: TextReference, + val isAutoResize: Boolean, + val centered: Boolean, +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index c37307f136..1616968df4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -17,6 +18,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -39,6 +41,7 @@ fun SimpleTextField( textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color), placeholderColor: Color = TangemTheme.colors.text.disabled, readOnly: Boolean = false, + centered: Boolean = false, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, isValuePasted: Boolean = false, onValuePastedTriggerDismiss: () -> Unit = {}, @@ -80,6 +83,8 @@ fun SimpleTextField( onValuePastedTriggerDismiss() } } + var textStyle = textStyle.copy(color = color) + if (centered) textStyle = textStyle.copy(textAlign = TextAlign.Center) BasicTextField( value = textFieldValue, @@ -91,7 +96,7 @@ fun SimpleTextField( if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text) }, - textStyle = textStyle.copy(color = color), + textStyle = textStyle, cursorBrush = SolidColor(TangemTheme.colors.text.primary1), singleLine = singleLine, readOnly = readOnly, @@ -105,6 +110,7 @@ fun SimpleTextField( value = value, textStyle = textStyle, textValue = textValue, + centered = centered, color = placeholderColor, ) }, @@ -118,10 +124,11 @@ private fun SimpleTextPlaceholder( placeholder: TextReference?, value: String, textStyle: TextStyle, + centered: Boolean, textValue: @Composable () -> Unit, color: Color = TangemTheme.colors.text.disabled, ) { - Box { + Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) { if (value.isBlank() && placeholder != null) { AnimatedContent( targetState = placeholder, diff --git a/features/account/api/.gitignore b/features/account/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/api/build.gradle.kts b/features/account/api/build.gradle.kts new file mode 100644 index 0000000000..349aee77c0 --- /dev/null +++ b/features/account/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.api" +} + +dependencies { + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt new file mode 100644 index 0000000000..1f6de5f0b2 --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface AccountCreateEditComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + sealed interface Params { + + data object Create : Params + data object Edit : Params + } +} \ No newline at end of file diff --git a/features/account/impl/.gitignore b/features/account/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/account/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts new file mode 100644 index 0000000000..55714f3021 --- /dev/null +++ b/features/account/impl/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.account.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.account.api) + + /** Core modules */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.models) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt new file mode 100644 index 0000000000..872b7af23f --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -0,0 +1,54 @@ +package com.tangem.features.account.createedit + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class AccountCreateEditModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(TODO()) + + init { + params + } + + fun unsaveChangeDialog() { + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_first), + onClick = {}, + ) + val secondAction = EventMessageAction( + title = resourceReference(R.string.account_unsaved_dialog_action_second), + onClick = { router.pop() }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_unsaved_dialog_title), + message = resourceReference(R.string.account_unsaved_dialog_message_create), + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt new file mode 100644 index 0000000000..06725717ad --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.account.createedit + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.ui.AccountCreateEditContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAccountCreateEditComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: AccountCreateEditComponent.Params, +) : AppComponentContext by appComponentContext, AccountCreateEditComponent { + + private val model: AccountCreateEditModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AccountCreateEditContent( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : AccountCreateEditComponent.Factory { + override fun create( + context: AppComponentContext, + params: AccountCreateEditComponent.Params, + ): DefaultAccountCreateEditComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt new file mode 100644 index 0000000000..ab899b3f5c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/di/AccountCreateEditModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.createedit.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.createedit.AccountCreateEditModel +import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountCreateEditModule { + + @Binds + fun bindAccountCreateEditComponentFactory( + impl: DefaultAccountCreateEditComponent.Factory, + ): AccountCreateEditComponent.Factory + + @Binds + @IntoMap + @ClassKey(AccountCreateEditModel::class) + fun bindAccountCreateEditModel(model: AccountCreateEditModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt new file mode 100644 index 0000000000..4ef4877b97 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -0,0 +1,42 @@ +package com.tangem.features.account.createedit.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.account.CryptoPortfolioIcon +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class AccountCreateEditUM( + val title: TextReference = TextReference.EMPTY, + val account: Account = Account(), + val colorsState: Colors, + val iconsState: Icons, + val buttonState: Button = Button(), + val onCloseClick: () -> Unit = {}, +) { + + data class Account( + val name: TextReference = TextReference.EMPTY, + val portfolioIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + val derivationInfo: TextReference = TextReference.EMPTY, + val inputPlaceholder: TextReference = TextReference.EMPTY, + val onNameChange: (String) -> Unit = {}, + ) + + data class Colors( + val selected: CryptoPortfolioIcon.Color, + val list: ImmutableList = persistentListOf(), + val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit = {}, + ) + + data class Icons( + val selected: CryptoPortfolioIcon.Icon, + val list: ImmutableList = persistentListOf(), + val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit = {}, + ) + + data class Button( + val isButtonEnabled: Boolean = false, + val onConfirmClick: () -> Unit = {}, + val text: TextReference = TextReference.EMPTY, + ) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt new file mode 100644 index 0000000000..dd927df0f0 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -0,0 +1,344 @@ +package com.tangem.features.account.createedit.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.account.getResId +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.fields.AutoSizeTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account +import kotlinx.collections.immutable.toImmutableList + +@Suppress("LongMethod", "MagicNumber") +@Composable +internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = state.title.resolveReference(), + onBackClick = state.onCloseClick, + iconRes = R.drawable.ic_close_24, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f), + + ) { + AccountSummary(state.account, state.account.onNameChange) + SpacerH24() + AccountColor(state.colorsState) + SpacerH24() + AccountIcon(state.iconsState) + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = 8.dp), + text = state.account.derivationInfo.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.buttonState.isButtonEnabled, + text = state.buttonState.text.resolveReference(), + onClick = state.buttonState.onConfirmClick, + ) + } +} + +@Composable +private fun AccountSummary(account: AccountCreateEditUM.Account, onNameChange: (String) -> Unit) { + Column( + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(24.dp)) + + AccountIcon(account) + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResourceSafe(R.string.account_form_name), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(2.dp)) + + AutoSizeTextField( + centered = true, + textStyle = TangemTheme.typography.head, + placeholder = account.inputPlaceholder, + value = account.name.resolveReference(), + singleLine = true, + onValueChange = onNameChange, + ) + SpacerH(20.dp) + } +} + +@Composable +private fun AccountIcon(account: AccountCreateEditUM.Account) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(88.dp) + .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) + .background(account.portfolioIcon.color.getUiColor()), + ) { + val icon = account.portfolioIcon.value + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = account.name.resolveReference().first().uppercase(), + style = TangemTheme.typography.head, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = Modifier.size(44.dp), + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action), + ) { + val columns = GridCells.Fixed(6) + val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp) + LazyVerticalGrid( + columns = columns, + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + itemsIndexed(colorsState.list) { index, color -> + val isSelected = color == colorsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { colorsState.onColorSelect(color) }) + .size(48.dp), + ) { + if (isSelected) { + Box( + modifier = Modifier + .size(47.dp) + .border(2.dp, color.getUiColor(), shape = CircleShape), + ) + Box( + modifier = Modifier + .size(36.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } else { + Box( + modifier = Modifier + .size(40.dp) + .background(color = color.getUiColor(), shape = CircleShape), + ) + } + } + } + } + } +} + +@Suppress("LongMethod", "MagicNumber") +@Composable +private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { + Box( + Modifier + .clip(RoundedCornerShape(16.dp)) + .fillMaxWidth() + .background(TangemTheme.colors.background.action) + .padding(8.dp), + ) { + val columns = GridCells.Fixed(6) + LazyVerticalGrid( + columns = columns, + ) { + itemsIndexed(iconsState.list) { index, icon -> + val isSelected = icon == iconsState.selected + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .wrapContentSize() + .clickable(onClick = { iconsState.onIconSelect(icon) }) + .size(52.dp), + ) { + if (isSelected) { + val borderColor: Color + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + borderColor = TangemTheme.colors.icon.accent + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + borderColor = TangemTheme.colors.icon.informative + iconTint = TangemTheme.colors.icon.secondary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(44.dp) + .border(2.dp, borderColor, shape = CircleShape), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } else { + val iconTint: Color + val backgroundTint: Color + if (index == 0) { + iconTint = TangemTheme.colors.icon.accent + backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + } else { + iconTint = TangemTheme.colors.text.tertiary + backgroundTint = TangemTheme.colors.field.focused + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .background(color = backgroundTint, shape = CircleShape), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + tint = iconTint, + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountCreateEditUM) { + TangemThemePreview { + AccountCreateEditContent(state = params) + } +} + +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + val colors = CryptoPortfolioIcon.Color.entries.toImmutableList() + val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList() + var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount() + val first = AccountCreateEditUM( + title = stringReference("Add account"), + account = Account( + portfolioIcon = portfolioIcon, + derivationInfo = stringReference("Account #03 — used for address derivation."), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + text = stringReference("Add account"), + ), + ) + add(first) + + portfolioIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.entries.random(), + ) + val second = AccountCreateEditUM( + title = stringReference("Edit account"), + account = Account( + portfolioIcon = portfolioIcon, + name = stringReference("Main account"), + derivationInfo = stringReference("Account #03 — used for address derivation."), + ), + colorsState = AccountCreateEditUM.Colors( + selected = portfolioIcon.color, + list = colors.toImmutableList(), + ), + iconsState = AccountCreateEditUM.Icons( + selected = portfolioIcon.value, + list = icons.toImmutableList(), + ), + buttonState = AccountCreateEditUM.Button( + text = stringReference("Save"), + ), + ) + add(second) + }, +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index bab69533de..2e4a4b6f20 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -269,6 +269,9 @@ include(":features:create-wallet-selection:impl") include(":features:welcome:api") include(":features:welcome:impl") + +include(":features:account:api") +include(":features:account:impl") // endregion Feature modules // region Domain modules From 5a42d4a84d6485b1e052f38107b96f432f6224f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 13:45:57 +0500 Subject: [PATCH 048/165] Updated on 2026-08-14 --- .../core/ui/components/fields/SearchBar.kt | 9 ++ .../component/ChooseManagedTokensComponent.kt | 6 + .../DefaultChooseManagedTokensComponent.kt | 2 +- .../model/ChooseManagedTokensModel.kt | 4 +- features/send-v2/impl/build.gradle.kts | 1 + .../DefaultSendEntryPointComponent.kt | 105 +++++++++++++++--- .../send/v2/entrypoint/SendEntryRoute.kt | 11 ++ .../entrypoint/model/SendEntryPointModel.kt | 70 +++++------- 8 files changed, 148 insertions(+), 60 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 4366b157be..2c45500ae0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -11,10 +11,13 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor @@ -47,6 +50,7 @@ fun SearchBar( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } val interactionSource = remember { MutableInteractionSource() } BasicTextField( @@ -60,6 +64,7 @@ fun SearchBar( state.onActiveChange(false) } } + .focusRequester(focusRequester) .testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR), enabled = enabled, value = state.query, @@ -92,6 +97,10 @@ fun SearchBar( ) }, ) + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } @Suppress("LongParameterList") diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index d17cd2b862..c5d28df18c 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -13,11 +13,17 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val selectedCurrency: CryptoCurrency?, val source: Source, val showSendViaSwapNotification: Boolean, + val callback: ModelCallback? = null, ) enum class Source { SendViaSwap, } + interface ModelCallback { + fun onResult() + fun onBack() + } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index 8e71b560bf..99206f1591 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -70,7 +70,7 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( cryptoCurrency = cryptoCurrency, shouldResetNavigation = params.selectedCurrency != null, ) - router.pop() + params.callback?.onResult() ?: router.pop() } }, ), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index fe98829fd5..53b4245820 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -89,13 +89,13 @@ internal class ChooseManagedTokensModel @Inject constructor( return ChooseManagedTokenUM( notificationUM = getNotification(), readContent = ManageTokensUM.ReadContent( - popBack = router::pop, + popBack = { params.callback?.onBack() ?: router.pop() }, isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), topBar = ManageTokensTopBarUM.ReadContent( title = resourceReference(R.string.common_choose_token), - onBackButtonClick = router::pop, + onBackButtonClick = { params.callback?.onBack() ?: router.pop() }, ), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index ef4d10fe2d..9d3830f696 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.features.txhistory.api) implementation(projects.features.nft.api) implementation(projects.features.swapV2.api) + implementation(projects.features.manageTokens.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index 4d66bc37aa..0985a678fb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -1,18 +1,29 @@ package com.tangem.features.send.v2.entrypoint -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.plus +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent -import com.tangem.features.send.v2.entrypoint.model.SendEntryPoint import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel import com.tangem.features.swap.v2.api.SendWithSwapComponent import dagger.assisted.Assisted @@ -24,9 +35,17 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( @Assisted private val params: SendEntryPointComponent.Params, sendWithSwapComponentFactory: SendWithSwapComponent.Factory, sendComponentFactory: SendComponent.Factory, + private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, ) : SendEntryPointComponent, AppComponentContext by appComponentContext { - private val model: SendEntryPointModel = getOrCreateModel(params = params) + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val model: SendEntryPointModel = getOrCreateModel(params = params, router = innerRouter) private val sendWithSwapComponent = sendWithSwapComponentFactory.create( context = child("sendEntrySendWithSwap"), @@ -46,18 +65,76 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( ), ) + private val childStack = childStack( + key = "sendEntryStack", + source = stackNavigation, + serializer = null, + initialConfiguration = SendEntryRoute.Send, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + getChildComponent( + configuration = configuration, + factoryContext = childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + @Composable override fun Content(modifier: Modifier) { - val sendEntryState by model.sendEntryPointState.collectAsStateWithLifecycle() + val childStackValue by childStack.subscribeAsState() - sendComponent.Content(modifier) + Children( + stack = childStackValue, + animation = stackAnimation { child -> + when (child.configuration) { + SendEntryRoute.Send, + SendEntryRoute.SendWithSwap, + -> fade() + is SendEntryRoute.ChooseToken -> slide(orientation = Orientation.Vertical) + fade() + } + }, + ) { child -> + child.instance.Content(modifier.fillMaxSize()) + } + } - AnimatedVisibility( - visible = sendEntryState == SendEntryPoint.SendWithSwap, - enter = fadeIn(), - exit = fadeOut(), - ) { - sendWithSwapComponent.Content(modifier) + private fun getChildComponent( + configuration: SendEntryRoute, + factoryContext: AppComponentContext, + ): ComposableContentComponent = when (configuration) { + is SendEntryRoute.ChooseToken -> getManagedTokensComponent( + componentContext = factoryContext, + showSendViaSwapNotification = configuration.showSendViaSwapNotification, + ) + SendEntryRoute.Send -> sendComponent + SendEntryRoute.SendWithSwap -> sendWithSwapComponent + } + + private fun getManagedTokensComponent( + componentContext: ComponentContext, + showSendViaSwapNotification: Boolean, + ): ChooseManagedTokensComponent { + return chooseManagedTokensComponentFactory.create( + context = childByContext(componentContext), + params = ChooseManagedTokensComponent.Params( + userWalletId = params.userWalletId, + initialCurrency = params.cryptoCurrency, + source = ChooseManagedTokensComponent.Source.SendViaSwap, + selectedCurrency = null, + showSendViaSwapNotification = showSendViaSwapNotification, + callback = model, + ), + ) + } + + private fun onChildBack() { + if (childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt new file mode 100644 index 0000000000..470cfb6215 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/SendEntryRoute.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.entrypoint + +import com.tangem.core.decompose.navigation.Route + +internal sealed class SendEntryRoute : Route { + data object Send : SendEntryRoute() + data object SendWithSwap : SendEntryRoute() + data class ChooseToken( + val showSendViaSwapNotification: Boolean, + ) : SendEntryRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt index 3bdbfcaf8d..984330ba7d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/model/SendEntryPointModel.kt @@ -1,90 +1,74 @@ package com.tangem.features.send.v2.entrypoint.model -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent -import com.tangem.features.send.v2.api.SendEntryPointComponent +import com.tangem.features.send.v2.entrypoint.SendEntryRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger import com.tangem.features.swap.v2.api.SendWithSwapComponent -import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn import jakarta.inject.Inject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Suppress("LongParameterList") @ModelScoped internal class SendEntryPointModel @Inject constructor( - paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - val appRouter: AppRouter, - private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, + val router: Router, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, -) : Model(), SendComponent.ModelCallback, SendWithSwapComponent.ModelCallback { +) : Model(), + SendComponent.ModelCallback, + SendWithSwapComponent.ModelCallback, + ChooseManagedTokensComponent.ModelCallback { - private val params: SendEntryPointComponent.Params = paramsContainer.require() - - val sendEntryPointState: StateFlow - field = MutableStateFlow(SendEntryPoint.SendVanilla) - - private var swapChooseTokenListenerJobHolder = JobHolder() + private var lastSavedAmount = "" override fun onConvertToAnotherToken(lastAmount: String) { + lastSavedAmount = lastAmount modelScope.launch { val showSendViaSwapNotification = shouldShowNotificationUseCase( NotificationId.SendViaSwapTokenSelectorNotification.key, ) - appRouter.push( - AppRoute.ChooseManagedTokens( - userWalletId = params.userWalletId, - initialCurrency = params.cryptoCurrency, - selectedCurrency = null, - source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, + router.push( + SendEntryRoute.ChooseToken( showSendViaSwapNotification = showSendViaSwapNotification, ), ) - observeChooseSelectToken(lastAmount) } } override fun onCloseSwap(lastAmount: String) { + lastSavedAmount = lastAmount modelScope.launch { if (lastAmount.isNotBlank()) { sendAmountUpdateTrigger.triggerUpdateAmount(lastAmount) } - triggerScreenUpdate(SendEntryPoint.SendVanilla) + router.replaceAll(SendEntryRoute.Send) } } - private fun observeChooseSelectToken(lastAmount: String) { - swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow - .onEach { currency -> - if (lastAmount.isNotBlank()) { - swapAmountUpdateTrigger.triggerUpdateAmount(lastAmount) - } - triggerScreenUpdate(SendEntryPoint.SendWithSwap) + @Suppress("MagicNumber") + override fun onResult() { + modelScope.launch { + if (lastSavedAmount.isNotBlank()) { + swapAmountUpdateTrigger.triggerUpdateAmount(lastSavedAmount) } - .launchIn(modelScope) - .saveIn(swapChooseTokenListenerJobHolder) + // Workaround in order to execute correct exit animation on SendEntryRoute.ChooseToken + router.pop() + delay(10L) + router.replaceAll(SendEntryRoute.SendWithSwap) + } } - private fun triggerScreenUpdate(entry: SendEntryPoint) { - swapChooseTokenListenerJobHolder.cancel() - sendEntryPointState.update { entry } + override fun onBack() { + router.pop() } -} - -enum class SendEntryPoint { - SendVanilla, - SendWithSwap, } \ No newline at end of file From e6820c0898f2d67f4e9946ee0d59e4874cd9db6b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 15:45:04 +0500 Subject: [PATCH 049/165] Updated on 2026-08-14 --- features/send-v2/api/build.gradle.kts | 1 + .../analytics/CommonSendAnalyticEvents.kt | 22 +++++++++++++++---- .../CommonSendAmountAnalyticEvents.kt} | 10 ++++----- .../analytics/CommonSendFeeAnalyticEvents.kt} | 10 ++++----- .../send/v2/send/DefaultSendComponent.kt | 2 +- .../v2/send/analytics/SendAnalyticEvents.kt | 2 +- .../v2/send/confirm/model/SendConfirmModel.kt | 4 ++-- ...endConfirmationNotificationsTransformer.kt | 4 ++-- ...dConfirmationNotificationsTransformerV2.kt | 4 ++-- .../features/send/v2/send/model/SendModel.kt | 4 ++-- .../success/model/SendConfirmSuccessModel.kt | 9 +++++--- .../v2/sendnft/DefaultNFTSendComponent.kt | 2 +- .../analytics/NFTSendAnalyticEvents.kt | 2 +- .../confirm/model/NFTSendConfirmModel.kt | 6 ++--- ...endConfirmationNotificationsTransformer.kt | 4 ++-- .../amount/model/SendAmountModel.kt | 8 +++---- .../destination/model/SendDestinationModel.kt | 4 ++-- .../subcomponents/fee/model/SendFeeModel.kt | 15 +++++++------ 18 files changed, 66 insertions(+), 47 deletions(-) rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/common => api/src/main/java/com/tangem/features/send/v2/api}/analytics/CommonSendAnalyticEvents.kt (87%) rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt => api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt} (70%) rename features/send-v2/{impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt => api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt} (81%) diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index c1a6e00fc9..454fcee782 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.analytics.models) /** Common */ implementation(projects.common.ui) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt similarity index 87% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index acaf68a42f..7b355b9c83 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.common.analytics +package com.tangem.features.send.v2.api.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN @@ -6,9 +6,9 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM /** - * Send screen analytics + * Send analytics */ -internal sealed class CommonSendAnalyticEvents( +sealed class CommonSendAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -114,12 +114,26 @@ internal sealed class CommonSendAnalyticEvents( ), ) + /** Token chosen to convert with sending */ + data class TokenChosen( + val categoryName: String, + val token: String, + val blockchain: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Token chosen", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) + companion object { const val SEND_CATEGORY = "Token / Send" const val NFT_SEND_CATEGORY = "NFT" } - internal enum class SendScreenSource { + enum class SendScreenSource { Address, Amount, Fee, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt similarity index 70% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt index 97d65a88e9..55d91be4b5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/amount/analytics/CommonSendAmountAnalyticEvents.kt @@ -1,9 +1,9 @@ -package com.tangem.features.send.v2.subcomponents.amount.analytics +package com.tangem.features.send.v2.api.subcomponents.amount.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE -internal sealed class SendAmountAnalyticEvents( +sealed class CommonSendAmountAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -13,7 +13,7 @@ internal sealed class SendAmountAnalyticEvents( data class SelectedCurrency( val categoryName: String, val type: SelectedCurrencyType, - ) : SendAmountAnalyticEvents( + ) : CommonSendAmountAnalyticEvents( category = categoryName, event = "Selected Currency", params = mapOf(TYPE to type.value), @@ -22,9 +22,9 @@ internal sealed class SendAmountAnalyticEvents( /** Max amount button clicked */ data class MaxAmountButtonClicked( val categoryName: String, - ) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") + ) : CommonSendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") - internal enum class SelectedCurrencyType(val value: String) { + enum class SelectedCurrencyType(val value: String) { Token("Token"), AppCurrency("App Currency"), } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt similarity index 81% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt index c65789aaa4..579c005570 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/analytics/CommonSendFeeAnalyticEvents.kt @@ -1,9 +1,9 @@ -package com.tangem.features.send.v2.subcomponents.fee.analytics +package com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -internal sealed class SendFeeAnalyticEvents( +sealed class CommonSendFeeAnalyticEvents( category: String, event: String, params: Map = mapOf(), @@ -15,7 +15,7 @@ internal sealed class SendFeeAnalyticEvents( data class SelectedFee( override val categoryName: String, val feeType: AnalyticsParam.FeeType, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Fee Selected", params = mapOf("Fee Type" to feeType.value), @@ -24,7 +24,7 @@ internal sealed class SendFeeAnalyticEvents( /** Custom fee selected */ data class CustomFeeButtonClicked( override val categoryName: String, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Custom Fee Clicked", ) @@ -32,7 +32,7 @@ internal sealed class SendFeeAnalyticEvents( /** Custom fee edited */ data class GasPriceInserter( override val categoryName: String, - ) : SendFeeAnalyticEvents( + ) : CommonSendFeeAnalyticEvents( category = categoryName, event = "Gas Price Inserted", ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 9cc15c97bb..013178fc55 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -24,10 +24,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 30a9234904..120a1808f8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -7,7 +7,7 @@ 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 import com.tangem.core.ui.extensions.capitalize -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents /** * Send screen analytics diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 4f8e762e40..343cd6d29e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -38,6 +38,8 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration @@ -48,8 +50,6 @@ import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificat import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index 8e8987f2f8..98b863e56e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -14,10 +14,10 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.utils.formatFooterFiatFee import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt index a5935435c3..22e99488fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -12,12 +12,12 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.api.utils.formatFooterFiatFee import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index de07147e93..7af880d59a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -41,6 +41,8 @@ import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent @@ -48,8 +50,6 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt index 663d111b4f..80bdafb04f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt @@ -12,14 +12,17 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject @Stable diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index 60fe6511a2..c4ecdee6e2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -20,10 +20,10 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.SendContent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt index de2ae7cbb4..f542b5f217 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt @@ -7,7 +7,7 @@ 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 import com.tangem.core.ui.extensions.capitalize -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents /** * Send screen analytics diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 734bc45601..ae95518260 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -22,23 +22,23 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.features.nft.entity.NFTSendSuccessTrigger import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.analytics.NFTSendAnalyticHelper diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt index fda1601aa9..9232bc21da 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.utils.formatFooterFiatFee import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 28b1e1cc38..72f75f5424 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -30,14 +30,14 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents.SelectedCurrencyType import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateListener -import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents -import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType import com.tangem.features.send.v2.subcomponents.fee.SendFeeData import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -218,7 +218,7 @@ internal class SendAmountModel @Inject constructor( ), ) analyticsEventHandler.send( - SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), + CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), ) } @@ -229,7 +229,7 @@ internal class SendAmountModel @Inject constructor( override fun onAmountNext() { (uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected -> analyticsEventHandler.send( - SendAmountAnalyticEvents.SelectedCurrency( + CommonSendAmountAnalyticEvents.SelectedCurrency( categoryName = analyticsCategoryName, type = if (isFiatSelected) { SelectedCurrencyType.AppCurrency diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index db3ba80d27..b457df2b9a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -26,12 +26,12 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index e1f722720f..5cad0c8511 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -11,14 +11,13 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase -import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.NonceInserted +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadListener -import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents -import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter import com.tangem.features.send.v2.subcomponents.fee.model.transformers.* import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType @@ -112,7 +111,7 @@ internal class SendFeeModel @Inject constructor( updateFeeNotifications() if (feeType == FeeType.Custom) { analyticsEventHandler.send( - SendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName), + CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName), ) } } @@ -155,10 +154,12 @@ internal class SendFeeModel @Inject constructor( val isCustomFeeEdited = feeSelectorUM.selectedFee?.amount?.value != feeSelectorUM.fees.normal.amount.value if (feeSelectorUM.selectedType == FeeType.Custom && isCustomFeeEdited) { - analyticsEventHandler.send(GasPriceInserter(categoryName = analyticsCategoryName)) + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.GasPriceInserter(categoryName = analyticsCategoryName), + ) } analyticsEventHandler.send( - SendFeeAnalyticEvents.SelectedFee( + CommonSendFeeAnalyticEvents.SelectedFee( categoryName = analyticsCategoryName, feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM), ), @@ -166,7 +167,7 @@ internal class SendFeeModel @Inject constructor( if (feeSelectorUM.nonce != null) { analyticsEventHandler.send( - NonceInserted( + CommonSendAnalyticEvents.NonceInserted( categoryName = analyticsCategoryName, token = cryptoCurrencyStatus.currency.symbol, blockchain = cryptoCurrencyStatus.currency.network.name, From 3a641b3edc225a5efa278351905736cf11bd13b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 14:21:00 +0300 Subject: [PATCH 050/165] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 11 ++++++ .../tangem/common/constants/TestConstants.kt | 5 +++ .../common/extensions/BaseTestCaseExt.kt | 14 ++++--- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 37 ++++++++++++------- .../kotlin/com/tangem/tests/HideTokenTest.kt | 3 +- .../com/tangem/tests/OrganizeTokensTest.kt | 37 +++++++++++++++---- .../kotlin/com/tangem/tests/StoriesTest.kt | 5 +-- .../com/tangem/tests/TermsOfServiceTest.kt | 6 +-- app/src/main/assets/tangem-app-config | 2 +- tangem-android-tools | 2 +- 10 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index d42aeddab4..57c7c6cd46 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -15,8 +15,11 @@ import com.kaspersky.kaspresso.testcases.api.testcase.TestCase import com.tangem.common.allure.FailedStepScreenshotInterceptor import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule @@ -40,6 +43,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var apiConfigsManager: ApiConfigsManager + @Inject + lateinit var appPreferencesStore: AppPreferencesStore + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -73,6 +79,11 @@ abstract class BaseTestCase : TestCase( additionalAfterSection: () -> Unit = {}, ) = before { hiltRule.inject() + runBlocking { + appPreferencesStore.editData { mutablePreferences -> mutablePreferences.set( + key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, value = false + ) } + } apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) Intents.init() diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt new file mode 100644 index 0000000000..29774a64fd --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -0,0 +1,5 @@ +package com.tangem.common.constants + +object TestConstants { + const val TOTAL_BALANCE = "$3,299.18" +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt index bff5addff5..72637225d9 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/BaseTestCaseExt.kt @@ -2,14 +2,16 @@ package com.tangem.common.extensions import com.tangem.common.BaseTestCase -fun BaseTestCase.swipeToCloseApp() { - +fun BaseTestCase.swipeUp( + startHeightRatio: Float = 0.8f, + endHeightRatio: Float = 0.03f, + steps: Int = 15 +) { device.uiDevice.swipe( device.uiDevice.displayWidth / 2, - device.uiDevice.displayHeight / 2, + (device.uiDevice.displayHeight * startHeightRatio).toInt(), device.uiDevice.displayWidth / 2, - device.uiDevice.displayHeight / 30, - 15 + (device.uiDevice.displayHeight * endHeightRatio).toInt(), + steps ) - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 1c05fa58dc..fb942f0550 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarios import com.tangem.common.utils.setWireMockScenarioState @@ -24,7 +25,7 @@ class BuyTokenTest : BaseTestCase() { } ).run { val tokenTitle = "Bitcoin" - val balance = "$184.85" + val balance = TOTAL_BALANCE resetWireMockScenarios() @@ -67,13 +68,15 @@ class BuyTokenTest : BaseTestCase() { fun validateCurrencySelectorTest() { setupHooks().run { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE val popularFiatsTitle = "Popular Fiats" val otherCurrenciesTitle = "Other currencies" val australianDollar = "AUD" val fiatAmount = "1" val tokenAmount = "POL 488.24938338" + resetWireMockScenarios() + step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -155,11 +158,13 @@ class BuyTokenTest : BaseTestCase() { fun validateBuyTokenScreenTest() { setupHooks().run { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE val euro = "EUR" val fiatAmount = "1" val tokenAmount = "POL 488.24938338" + resetWireMockScenarios() + step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -226,10 +231,12 @@ class BuyTokenTest : BaseTestCase() { fun validateResidenceSettingsScreenTest() { setupHooks().run { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE val country = "Albania" val unavailableCountry = "Lebanon" + resetWireMockScenarios() + step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -305,7 +312,7 @@ class BuyTokenTest : BaseTestCase() { fun validateProvidersScreenTest() { setupHooks().run { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE val paymentMethod = "Card" val fiatAmount = "1" val providerNameMercuryo = "Mercuryo" @@ -314,6 +321,8 @@ class BuyTokenTest : BaseTestCase() { val bestRate = "Best rate" val rate = "-0.00%" + resetWireMockScenarios() + step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -347,13 +356,6 @@ class BuyTokenTest : BaseTestCase() { step("Open 'Select Provider' bottom sheet") { onBuyTokenDetailsScreen { providerTitle.performClick() } } - step("Assert available provider name is displayed") { - onSelectProviderBottomSheet { - flakySafely(timeoutMs = 20_000) { - availableProviderItem.assertIsDisplayed() - } - } - } step("Assert unavailable provider name is displayed") { onSelectProviderBottomSheet { flakySafely(timeoutMs = 20_000) { @@ -361,6 +363,13 @@ class BuyTokenTest : BaseTestCase() { } } } + step("Assert available provider name is displayed") { + onSelectProviderBottomSheet { + flakySafely(timeoutMs = 20_000) { + availableProviderItem.assertIsDisplayed() + } + } + } step("Click on 'Expand payment methods' button") { onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() } } @@ -407,13 +416,15 @@ class BuyTokenTest : BaseTestCase() { fun validatePaymentMethodScreenTest() { setupHooks().run { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE val card = "Card" val googlePay = "Google Pay" val invoiceRevolutPay = "Invoice Revolut Pay" val sepa = "Sepa" val fiatAmount = "1" + resetWireMockScenarios() + step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt index 6bbf318ec7..706bd497d5 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/HideTokenTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* @@ -17,7 +18,7 @@ class HideTokenTest : BaseTestCase() { @Test fun hideWalletTokenByHideButtonTest() { val tokenTitle = "Polygon" - val balance = "$184.85" + val balance = TOTAL_BALANCE setupHooks().run { step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 6d89ee640d..44cab70be3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -2,7 +2,9 @@ package com.tangem.tests import androidx.compose.ui.test.onAllNodesWithText import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeUp import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.onMainScreen import com.tangem.screens.onOrganizeTokensScreen @@ -27,6 +29,9 @@ class OrganizeTokensTest : BaseTestCase() { step("Click on 'Synchronize addresses' button" ) { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -48,6 +53,9 @@ class OrganizeTokensTest : BaseTestCase() { step("Assert tokens were grouped on 'Main screen'") { onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -79,7 +87,7 @@ class OrganizeTokensTest : BaseTestCase() { setupHooks().run { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" - val balance = "$184.85" + val balance = TOTAL_BALANCE step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -95,6 +103,9 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -127,6 +138,9 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -158,7 +172,8 @@ class OrganizeTokensTest : BaseTestCase() { val ethereumTitle = "Ethereum" val bitcoinTitle = "Bitcoin" val polygonTitle = "Polygon" - val balance = "$184.85" + val polExMaticTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) } @@ -175,6 +190,9 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() } } + step("Swipe to 'Organize tokens' button") { + swipeUp() + } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } } @@ -183,6 +201,7 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(bitcoinTitle, 1).assertIsDisplayed() tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed() tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 4).assertIsDisplayed() } } step("Click 'By Balance' button") { @@ -192,9 +211,10 @@ class OrganizeTokensTest : BaseTestCase() { } step("Check positions of tokens by balance on 'Organize tokens' screen") { onOrganizeTokensScreen { - tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed() - tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed() - tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 3).assertIsDisplayed() + tokenWithTitleAndPosition(bitcoinTitle, 4).assertIsDisplayed() } } step("Click 'Apply' button") { @@ -202,9 +222,10 @@ class OrganizeTokensTest : BaseTestCase() { } step("Check positions of tokens by balance on 'Organize tokens' screen") { onMainScreen { - tokenWithTitleAndPosition(polygonTitle, 0).assertIsDisplayed() - tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() - tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed() + tokenWithTitleAndPosition(polExMaticTitle, 1).assertIsDisplayed() + tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() + tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 63a4706cb0..d87e1be98c 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -7,15 +7,14 @@ import com.tangem.screens.onDisclaimerScreen import com.tangem.screens.onStoriesScreen import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.intent.KIntent -import org.junit.Test @HiltAndroidTest class StoriesTest : BaseTestCase() { - @Test + // @Test fun clickOnOrderButtonTest() = setupHooks().run { - val buyWalletUrl = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + val buyWalletUrl = "https://buy.tangem.com/" onDisclaimerScreen { step("Click on 'Accept' button") { acceptButton.clickWithAssertion() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt index 0e596d4528..7f729f1c3c 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/TermsOfServiceTest.kt @@ -3,7 +3,7 @@ package com.tangem.tests import androidx.test.InstrumentationRegistry.getTargetContext import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeToCloseApp +import com.tangem.common.extensions.swipeUp import com.tangem.screens.onDisclaimerScreen import com.tangem.screens.onStoriesScreen import dagger.hilt.android.testing.HiltAndroidTest @@ -67,7 +67,7 @@ class TermsOfServiceTest : BaseTestCase() { device.uiDevice.pressRecentApps() } step("Stop app by swipe") { - swipeToCloseApp() + swipeUp(startHeightRatio = 0.5f) } step("Launch app") { device.apps.launch(packageName) @@ -90,7 +90,7 @@ class TermsOfServiceTest : BaseTestCase() { device.uiDevice.pressRecentApps() } step("Stop app by swipe") { - swipeToCloseApp() + swipeUp(startHeightRatio = 0.5f) } step("Launch app") { device.apps.launch(packageName) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 3ac868e93f..e87768263c 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 +Subproject commit e87768263c0a79018958b0872940c604180a623a diff --git a/tangem-android-tools b/tangem-android-tools index bc4cd43085..428b83bb37 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 +Subproject commit 428b83bb378b615209e23afa05c88c454d06a9f1 From f690d13c82b0cea631a4afd065a6deb2b2ebf95c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 11:28:55 +0000 Subject: [PATCH 051/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7ab02a4628..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1126" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-510" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c5e731d5d37c86924712052fd999a5f91ed1ae1e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 16:31:26 +0500 Subject: [PATCH 052/165] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 10 ++++ .../send/v2/api/entity/FeeSelectorUM.kt | 16 +++++- .../send/v2/send/DefaultSendComponent.kt | 6 +- .../v2/send/analytics/SendAnalyticEvents.kt | 11 ++++ .../features/send/v2/send/model/SendModel.kt | 7 +++ features/swap-v2/impl/build.gradle.kts | 1 + .../impl/amount/SwapAmountBlockComponent.kt | 16 +----- .../analytics/SwapAmountAnalyticEvents.kt | 27 +++++++++ .../amount/model/SwapAmountClickIntents.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 55 ++++++++++++++++++- .../ui/preview/SwapAmountClickIntentsStub.kt | 2 + .../DefaultSendWithSwapComponent.kt | 25 ++++++--- .../analytics/SendWithSwapAnalyticEvents.kt | 34 ++++++++++++ .../confirm/model/SendWithSwapConfirmModel.kt | 48 +++++++++++++++- .../sendviaswap/model/SendWithSwapModel.kt | 2 + 15 files changed, 233 insertions(+), 28 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 8eed0785bc..b85be1b1aa 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -120,6 +120,12 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, ) : TxSentFrom("NFT"), TxData + + data class SendWithSwap( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Send&Swap"), TxData } sealed interface TxData { @@ -224,5 +230,9 @@ sealed class AnalyticsParam { const val STANDARD = "Standard" const val NO_COLLECTION = "No collection" const val EMULATION_STATUS = "Emulation Status" + const val SEND_TOKEN = "Send Token" + const val RECEIVE_TOKEN = "Receive Token" + const val SEND_BLOCKCHAIN = "Send Blockchain" + const val RECEIVE_BLOCKCHAIN = "Receive Blockchain" } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index 6386d23721..ca50585ecf 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -3,9 +3,11 @@ package com.tangem.features.send.v2.api.entity import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.entity.FeeItem.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal import java.math.BigInteger @@ -31,7 +33,19 @@ sealed class FeeSelectorUM { val feeExtraInfo: FeeExtraInfo, val feeFiatRateUM: FeeFiatRateUM?, val feeNonce: FeeNonce, - ) : FeeSelectorUM() + ) : FeeSelectorUM() { + fun toAnalyticType(): AnalyticsParam.FeeType = when (fees) { + is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed + is TransactionFee.Choosable -> when (selectedFeeItem) { + is Suggested, + is Custom, + -> AnalyticsParam.FeeType.Custom + is Fast -> AnalyticsParam.FeeType.Max + is Market -> AnalyticsParam.FeeType.Normal + is Slow -> AnalyticsParam.FeeType.Min + } + } + } } @Immutable diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 013178fc55..47c52407c5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -90,13 +90,15 @@ internal class DefaultSendComponent @AssistedInject constructor( ) { stack -> componentScope.launch { when (val activeComponent = stack.active.instance) { - is SendConfirmComponent -> if (model.currentRoute.value.isEditMode) { + is SendConfirmComponent -> { analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, ), ) - activeComponent.updateState(model.uiState.value) + if (model.currentRoute.value.isEditMode) { + activeComponent.updateState(model.uiState.value) + } } is SendAmountComponent -> { analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 120a1808f8..3a705b2270 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -32,4 +32,15 @@ internal sealed class SendAnalyticEvents( NONCE to nonceNotEmpty.toString().capitalize(), ), ) + + data class ConvertTokenButtonClicked( + val token: String, + val blockchain: String, + ) : SendAnalyticEvents( + event = "Button - Convert Token", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 7af880d59a..f0a63844c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -51,6 +51,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent import com.tangem.features.send.v2.send.ui.state.SendUM @@ -227,6 +228,12 @@ internal class SendModel @Inject constructor( } override fun onConvertToAnotherToken(lastAmount: String) { + analyticsEventHandler.send( + SendAnalyticEvents.ConvertTokenButtonClicked( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) params.callback?.onConvertToAnotherToken(lastAmount = lastAmount) } diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index ab1ccbbe75..fed1e814da 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.configToggles) implementation(projects.core.datasource) + implementation(projects.core.analytics) /** Common */ implementation(projects.common.ui) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt index 0d6966910d..4df2de3238 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountBlockComponent.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe @@ -69,20 +68,7 @@ internal class SwapAmountBlockComponent( isClickEnabled = isClickEnabled, onClick = onClick, onFinishAnimation = model::onFinishAnimation, - onProviderSelectClick = { - val amountUM = model.uiState.value as? SwapAmountUM.Content ?: return@SwapAmountBlockContent - val selectedProvider = amountUM.selectedQuote.provider ?: return@SwapAmountBlockContent - val cryptoCurrency = params.secondaryCryptoCurrency ?: return@SwapAmountBlockContent - - model.bottomSheetNavigation.activate( - SwapChooseProviderConfig( - providers = amountUM.swapQuotes, - cryptoCurrency = cryptoCurrency, - selectedProvider = selectedProvider, - userCountry = model.userCountry, - ), - ) - }, + onProviderSelectClick = model::onProviderClick, ) bottomSheet.child?.instance?.BottomSheet() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt new file mode 100644 index 0000000000..197bd8a27c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/analytics/SwapAmountAnalyticEvents.kt @@ -0,0 +1,27 @@ +package com.tangem.features.swap.v2.impl.amount.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER + +internal sealed class SwapAmountAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + data class ProviderSelectorClicked( + val categoryName: String, + ) : SwapAmountAnalyticEvents( + category = categoryName, + event = "Provider Clicked", + ) + + data class ProviderChosen( + val categoryName: String, + val providerName: String, + ) : SwapAmountAnalyticEvents( + category = categoryName, + event = "Provider Chosen", + params = mapOf(PROVIDER to providerName), + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index 7e5a767bb6..cf6b75586e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -9,4 +9,5 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onInfoClick() fun onSelectTokenClick() fun onSeparatorClick() + fun onProviderClick() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index efc0561da2..69fc128e10 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.swap.v2.impl.amount.model import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter @@ -10,6 +11,7 @@ import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -32,6 +34,7 @@ import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.impl.R @@ -39,6 +42,7 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChoo import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceListener import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener +import com.tangem.features.swap.v2.impl.amount.analytics.SwapAmountAnalyticEvents import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -85,6 +89,7 @@ internal class SwapAmountModel @Inject constructor( private val swapAmountReduceListener: SwapAmountReduceListener, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -145,6 +150,12 @@ internal class SwapAmountModel @Inject constructor( } override fun onProviderResult(quoteUM: SwapQuoteUM) { + analyticsEventHandler.send( + SwapAmountAnalyticEvents.ProviderChosen( + categoryName = params.analyticsCategoryName, + providerName = quoteUM.provider?.name.orEmpty(), + ), + ) uiState.transformerUpdate( SwapAmountSelectQuoteTransformer( quoteUM = quoteUM, @@ -199,6 +210,9 @@ internal class SwapAmountModel @Inject constructor( } override fun onMaxValueClick() { + analyticsEventHandler.send( + CommonSendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = params.analyticsCategoryName), + ) uiState.transformerUpdate( SwapAmountValueMaxTransformer( primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, @@ -219,6 +233,23 @@ internal class SwapAmountModel @Inject constructor( } override fun onAmountNext() { + val amountState = uiState.value.swapDirection.withSwapDirection( + onDirect = { uiState.value.primaryAmount.amountField }, + onReverse = { uiState.value.secondaryAmount.amountField }, + ) as? AmountState.Data + + amountState?.amountTextField?.isFiatValue?.let { isFiatSelected -> + analyticsEventHandler.send( + CommonSendAmountAnalyticEvents.SelectedCurrency( + categoryName = params.analyticsCategoryName, + type = if (isFiatSelected) { + CommonSendAmountAnalyticEvents.SelectedCurrencyType.AppCurrency + } else { + CommonSendAmountAnalyticEvents.SelectedCurrencyType.Token + }, + ), + ) + } saveResult() } @@ -264,10 +295,30 @@ internal class SwapAmountModel @Inject constructor( } } + override fun onProviderClick() { + val amountUM = uiState.value as? SwapAmountUM.Content ?: return + val selectedProvider = amountUM.selectedQuote.provider ?: return + val cryptoCurrency = params.secondaryCryptoCurrency ?: return + + analyticsEventHandler.send( + SwapAmountAnalyticEvents.ProviderSelectorClicked( + categoryName = params.analyticsCategoryName, + ), + ) + + bottomSheetNavigation.activate( + SwapChooseProviderConfig( + providers = amountUM.swapQuotes, + cryptoCurrency = cryptoCurrency, + selectedProvider = selectedProvider, + userCountry = userCountry, + ), + ) + } + private fun confirmSendWithSwapClose() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data - val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (primaryCryptoCurrencyStatus != null) { @@ -622,7 +673,7 @@ internal class SwapAmountModel @Inject constructor( }, isEnabled = state.isPrimaryButtonEnabled, onClick = { - saveResult() + onAmountNext() params.callback.onNextClick() }, ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 1c60ad739a..4c55f4759a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -12,6 +12,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSeparatorClick() {} + override fun onProviderClick() {} + override fun onAmountValueChange(value: String) {} override fun onAmountPasteTriggerDismiss() {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index e331657e56..ed82f2f734 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -12,6 +12,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.ObserveLifecycleMode import com.arkivanov.decompose.value.subscribe import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -21,6 +22,7 @@ import com.tangem.core.ui.decompose.getEmptyComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -44,6 +46,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( @Assisted private val params: SendWithSwapComponent.Params, private val sendDestinationComponentFactory: SendDestinationComponent.Factory, private val confirmComponentFactory: SendWithSwapConfirmComponent.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : SendWithSwapComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -80,15 +83,23 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SwapAmountComponent -> { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened(categoryName = model.analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.AddressScreenOpened(categoryName = model.analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> if (model.currentRoute.value.isEditMode) { - // todo send with swap analytics + analyticsEventHandler.send( + CommonSendAnalyticEvents.ConfirmationScreenOpened( + categoryName = model.analyticCategoryName, + ), + ) activeComponent.updateState(model.uiState.value) } } @@ -125,7 +136,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( title = resourceReference(R.string.common_send), currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow, secondaryCryptoCurrency = null, swapDirection = SwapDirection.Direct, @@ -148,7 +159,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( state = model.uiState.value.destinationUM, currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, title = resourceReference(R.string.send_recipient_label), userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, @@ -167,7 +178,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( appCurrency = model.appCurrency, userWallet = model.userWallet, callback = model, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, primaryCryptoCurrencyStatusFlow = model.primaryCryptoCurrencyStatusFlow, primaryFeePaidCurrencyStatusFlow = model.primaryFeePaidCurrencyStatusFlow, swapDirection = SwapDirection.Direct, @@ -182,7 +193,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( sendWithSwapUMFlow = model.uiState, currentRoute = model.currentRoute.filterIsInstance(), callback = model, - analyticsCategoryName = "", + analyticsCategoryName = model.analyticCategoryName, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt new file mode 100644 index 0000000000..ad1927f7cd --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -0,0 +1,34 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER +import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents + +internal sealed class SendWithSwapAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { + + data class TransactionScreenOpened( + val providerName: String, + val feeType: AnalyticsParam.FeeType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap In Progress Screen Opened", + params = mapOf( + PROVIDER to providerName, + "Commission" to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast", + SEND_TOKEN to fromToken.symbol, + RECEIVE_TOKEN to toToken.symbol, + SEND_BLOCKCHAIN to fromToken.network.name, + RECEIVE_BLOCKCHAIN to toToken.network.name, + ), + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 847f6d100b..5ebe7db905 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -9,6 +9,9 @@ import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -25,6 +28,8 @@ import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM @@ -43,6 +48,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmationNotificationsTransformer @@ -73,6 +79,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val swapAlertFactory: SwapAlertFactory, private val appRouter: AppRouter, + private val analyticsEventHandler: AnalyticsEventHandler, swapTransactionSenderFactory: SwapTransactionSender.Factory, paramsContainer: ParamsContainer, ) : Model(), FeeSelectorModelCallback, SendNotificationsComponent.ModelCallback { @@ -185,10 +192,22 @@ internal class SendWithSwapConfirmModel @Inject constructor( } fun showEditAmount() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Amount, + ), + ) router.push(SendWithSwapRoute.Amount(isEditMode = true)) } fun showEditDestination() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Address, + ), + ) router.push(SendWithSwapRoute.Destination(isEditMode = true)) } @@ -260,7 +279,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( txHash = txHash, networkId = primaryCurrencyStatus.currency.network.id, ).getOrNull().orEmpty() - + sendSuccessAnalytics() uiState.update { it.copy( confirmUM = ConfirmUM.Success( @@ -348,6 +367,33 @@ internal class SendWithSwapConfirmModel @Inject constructor( }.launchIn(modelScope) } + private fun sendSuccessAnalytics() { + val selectedProvider = confirmData.quote?.provider ?: return + val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency ?: return + val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return + val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return + val feeType = feeSelectorUM.toAnalyticType() + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.TransactionScreenOpened( + providerName = selectedProvider.name, + feeType = feeType, + fromToken = fromCurrency, + toToken = toCurrency, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.SendWithSwap( + blockchain = fromCurrency.network.name, + token = fromCurrency.symbol, + feeType = feeType, + ), + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + } + private fun configConfirmNavigation() { combine( flow = uiState, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index e370965fe4..ede2387f9d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM @@ -58,6 +59,7 @@ internal class SendWithSwapModel @Inject constructor( private val params: SendWithSwapComponent.Params = paramsContainer.require() + val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY val initialRoute = SendWithSwapRoute.Amount(false) val currentRoute = MutableStateFlow(initialRoute) From 9faf47bb8b69f93fb79a5f68c13de00234cca7c0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 18:39:30 +0500 Subject: [PATCH 053/165] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheetWithFooter.kt | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 479fe79469..643453a4bc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.core.ui.utils.toPx /** * Modal bottom sheet with [content], [footer] and optional [title]. @@ -154,6 +155,21 @@ inline fun BasicModalBottomSheetWit val initial = 0 val scrollState = rememberScrollState(initial = initial) + val isKeyboardOpen by rememberIsKeyboardVisible() + val buttonHeight = TangemTheme.dimens.spacing80 + val contentBottomPadding = TangemTheme.dimens.spacing80 + // Offset calculation for keyboard scroll adjustment: + // 1) Button height (footer) + // 2) Column content bottom padding + // 3) Additional spacing (40dp) for visual comfort when keyboard is open + val scrollOffset = buttonHeight.toPx() + buttonHeight.toPx() + 40.dp.toPx() + + LaunchedEffect(isKeyboardOpen) { + if (isKeyboardOpen) { + scrollState.animateScrollTo(scrollState.value + scrollOffset.toInt()) + } + } + Column( modifier = Modifier .systemBarsPadding() @@ -186,7 +202,7 @@ inline fun BasicModalBottomSheetWit Column( modifier = Modifier .verticalScroll(state = scrollState) - .padding(bottom = TangemTheme.dimens.spacing80), + .padding(bottom = contentBottomPadding), ) { content(model) } @@ -199,7 +215,7 @@ inline fun BasicModalBottomSheetWit Box( modifier = Modifier .fillMaxWidth() - .height(80.dp) + .height(buttonHeight) .align(Alignment.BottomCenter), ) { footer(model) From 8c4239c07f0b0fed22e30fc7d1721889812ce7a6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 16:06:26 +0000 Subject: [PATCH 054/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-446" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From d369d87c2ff1ff84a273ed07d3cb9f3c38f44bec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 5 Aug 2025 19:19:52 +0400 Subject: [PATCH 055/165] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 7 +- domain/account/build.gradle.kts | 3 +- .../domain/account/models/AccountList.kt | 57 +++- .../usecase/UpdateCryptoPortfolioUseCase.kt | 131 ++++++-- .../domain/account/models/AccountListTest.kt | 46 ++- .../UpdateCryptoPortfolioUseCaseTest.kt | 298 ++++++++++++++++++ .../tangem/domain/account/utils/AccountExt.kt | 8 + .../tangem/domain/models/account/Account.kt | 47 +++ 8 files changed, 565 insertions(+), 32 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 22e627efca..5c303041a0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase @@ -22,8 +23,10 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideUpdateCryptoPortfolioUseCase(): UpdateCryptoPortfolioUseCase { - return UpdateCryptoPortfolioUseCase() + fun provideUpdateCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): UpdateCryptoPortfolioUseCase { + return UpdateCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index d21a2a628e..cf1bc96831 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -16,8 +16,9 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.serialization) + testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) - testImplementation(deps.test.truth) testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index fdc9f393b0..e782ece215 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -5,6 +5,7 @@ import arrow.core.raise.either import arrow.core.raise.ensure import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable /** @@ -27,6 +28,42 @@ data class AccountList private constructor( val mainAccount: Account.CryptoPortfolio get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + /** + * Adds an account to the account list. + * If an account with the same identifier already exists, it will be replaced. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., maximum number of accounts exceeded). + * + * @param other the account to add or replace + */ + operator fun plus(other: Account): Either { + val isNewAccount = this.accounts.none { it.accountId == other.accountId } + val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId } + + return invoke( + userWallet = this.userWallet, + accounts = accounts, + totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + ) + } + + /** + * Removes the specified account from the account list. + * Returns a new [AccountList] instance with the updated accounts set, or a validation error if constraints are + * violated (e.g., the list becomes empty). + * + * @param other the account to remove + */ + operator fun minus(other: Account): Either { + return invoke( + userWallet = this.userWallet, + accounts = this.accounts.toMutableSet().apply { + removeIf { it.accountId == other.accountId } + }, + totalAccounts = this.totalAccounts - 1, + ) + } + /** * Represents possible errors that can occur when creating an `AccountList` */ @@ -54,10 +91,23 @@ data class AccountList private constructor( return "$tag: There should be at most one main crypto portfolio in the account list" } } + + @Serializable + data object ExceedsMaxAccountsCount : Error { + override fun toString(): String = "$tag: The number of accounts must not exceed 20" + } + + @Serializable + data object DuplicateAccountIds : Error { + override fun toString(): String = "$tag: Account list contains duplicate account IDs" + } } companion object { + private const val MAX_ACCOUNTS_COUNT = 20 + private const val MAX_MAIN_ACCOUNTS_COUNT = 1 + /** * Factory method to create an `AccountList` instance. * Validates the input to ensure the accounts list is not empty and contains exactly one main account. @@ -73,8 +123,10 @@ data class AccountList private constructor( ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } + ensure(accounts.size <= MAX_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount } + val mainAccountsCount = accounts.mainAccountsCount() - ensure(mainAccountsCount == 1) { + ensure(mainAccountsCount == MAX_MAIN_ACCOUNTS_COUNT) { if (mainAccountsCount == 0) { Error.MainAccountNotFound } else { @@ -82,6 +134,9 @@ data class AccountList private constructor( } } + val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size + ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } + AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index cec8cbc039..d73f3e9eaa 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -1,50 +1,129 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType +import arrow.core.raise.ensure +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon -import kotlin.random.Random +import com.tangem.domain.models.wallet.UserWalletId /** + * Use case for updating a crypto portfolio account. + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class UpdateCryptoPortfolioUseCase { +class UpdateCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** + * Updates a crypto portfolio account with the provided name and/or icon + * + * @param accountId the unique identifier of the account to update + * @param accountName the new name for the account (optional) + * @param icon the new icon for the account (optional) + * @return an [Either] containing the updated [Account.CryptoPortfolio] on success, or an [Error] on failure + */ suspend operator fun invoke( accountId: AccountId, - name: AccountName? = null, + accountName: AccountName? = null, icon: CryptoPortfolioIcon? = null, ): Either = either { - Account.CryptoPortfolio( - accountId = accountId, - name = name?.value ?: "Account", - accountIcon = icon ?: CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = Random.nextInt(from = 0, until = 21), - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), - ) - .mapLeft { Error.DataOperationFailed } - .bind() + ensure(accountName != null || icon != null) { Error.NothingToUpdate } - // TODO: [REDACTED_JIRA] - // Create a domain model AccountName - // Get the current account by [accountId] - // Create a new domain model Account from old data considering new parameters - // Save information in local storage - // Save to backend + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val account = accountList.accounts + .firstOrNull { it.accountId == accountId } as? Account.CryptoPortfolio + ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + + val updatedAccount = account + .setName(name = accountName) + .setIcon(icon = icon) + + val updatedAccounts = (accountList + updatedAccount) + .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) } + + saveAccounts(updatedAccounts) + + updatedAccount } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Account.CryptoPortfolio.setName(name: AccountName?): Account.CryptoPortfolio { + return if (name != null) this.copy(accountName = name) else this + } + + private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { + return if (icon != null) this.copy(accountIcon = icon) else this + } + + /** + * Represents possible errors that can occur during the update operation + */ sealed interface Error { - data object DataOperationFailed : Error + /** Error indicating that there is nothing to update */ + data object NothingToUpdate : Error { + override fun toString(): String = "Nothing to update: both account name and icon are null" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } + + /** + * Represents critical technical errors that can occur during the update operation. + * These errors are a consequence of an inconsistent state. + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + override fun toString(): String = "Accounts for $userWalletId are not created" + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { + override fun toString(): String = "Account list requirements not met: $cause" + } + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 2abafd8934..04bb22f7b6 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -3,7 +3,9 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.left import com.google.common.truth.Truth +import com.tangem.domain.account.utils.randomAccountId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import io.mockk.clearMocks import io.mockk.every @@ -92,6 +94,33 @@ class AccountListTest { ), ) }, + createAccounts(count = 20).let { + CreateTestModel( + accounts = it, + expected = AccountList( + userWallet = userWallet, + accounts = it, + totalAccounts = 20, + ), + ) + }, + CreateTestModel( + accounts = createAccounts(21), + expected = AccountList.Error.ExceedsMaxAccountsCount.left(), + ), + CreateTestModel( + accounts = setOf( + createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ), + createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = false, + ), + ), + expected = AccountList.Error.DuplicateAccountIds.left(), + ), ) } @@ -100,9 +129,22 @@ class AccountListTest { val expected: Either, ) - private fun createAccount(isMain: Boolean = false): Account.CryptoPortfolio { + private fun createAccounts(count: Int): Set { + return buildSet { + add(createAccount(isMain = true)) + repeat(count - 1) { + add(createAccount(isMain = false)) + } + } + } + + private fun createAccount( + accountId: AccountId = AccountId(value = randomAccountId(5), userWalletId = mockk()), + isMain: Boolean = false, + ): Account.CryptoPortfolio { return mockk { - every { isMainAccount } returns isMain + every { this@mockk.accountId } returns accountId + every { this@mockk.isMainAccount } returns isMain } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..6588216bc8 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,298 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.randomAccountId +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import kotlin.random.Random + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class UpdateCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should update crypto portfolio account with new name`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val updatedAccount = account.copy(accountName = newAccountName) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new icon`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, icon = newAccountIcon) + + // Assert + val updatedAccount = account.copy(accountIcon = newAccountIcon) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke should update crypto portfolio account with new name and icon`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.CaribbeanBlue, + ) + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon) + + // Assert + val updatedAccount = account.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val expected = updatedAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + @Test + fun `invoke if name and icon are null`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId) + + // Assert + val expected = Error.NothingToUpdate.left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(inverse = true) { + crudRepository.getAccounts(userWalletId = any()) + crudRepository.saveAccounts(accountList = any()) + } + } + + @Test + fun `invoke if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + + val newAccountName = AccountName("New name").getOrNull()!! + val exception = IllegalStateException("Test exception") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val accountList = None + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if getAccounts does not contain accountId`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount( + accountId = AccountId(value = "another-account-id", userWalletId = mockk()), + isMain = true, + ) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ) + .getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId = accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(accountList = any()) } + } + + @Test + fun `invoke if saveAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) + val userWalletId = accountId.userWalletId + val account = createAccount(accountId = accountId, isMain = true) + val accountList = AccountList( + userWallet = mockk(), + accounts = setOf(account), + totalAccounts = 1, + ).getOrNull()!! + + val newAccountName = AccountName("New name").getOrNull()!! + val updatedAccount = account.copy(accountName = newAccountName) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! + + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(accountList = updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId = accountId, accountName = newAccountName) + + // Assert + val expected = Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId = userWalletId) + crudRepository.saveAccounts(accountList = updatedAccountList) + } + } + + private fun createAccount( + accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), + accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + isMain: Boolean, + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = accountId, + name = "Test Account", + accountIcon = accountIcon, + derivationIndex = if (isMain) 0 else Random.nextInt(1, 21), + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt new file mode 100644 index 0000000000..1186faab17 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.account.utils + +fun randomAccountId(length: Int): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return (1..length) + .map { chars.random() } + .joinToString("") +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index bbea116526..8d7b786cca 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -60,6 +60,21 @@ sealed interface Account { val networksCount: Int get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + fun copy( + accountName: AccountName = this.name, + accountIcon: CryptoPortfolioIcon = this.icon, + isArchived: Boolean = this.isArchived, + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = this.accountId, + name = accountName, + icon = accountIcon, + derivationIndex = this.derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = this.cryptoCurrencyList, + ) + } + /** * Represents a list of tokens in the account * @@ -117,6 +132,38 @@ sealed interface Account { return either { val accountName = AccountName(name).mapLeft(::AccountNameError).bind() + invoke( + accountId = accountId, + accountName = accountName, + accountIcon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) + .bind() + } + } + + /** + * Constructor for creating a [CryptoPortfolio] instance + * + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param accountIcon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param isArchived indicates whether the account is archived + * @param cryptoCurrencyList list of tokens associated with the account + */ + @Suppress("LongParameterList") + operator fun invoke( + accountId: AccountId, + accountName: AccountName, + accountIcon: CryptoPortfolioIcon, + derivationIndex: Int, + isArchived: Boolean, + cryptoCurrencyList: CryptoCurrencyList, + ): Either { + return either { ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex } CryptoPortfolio( From 1d47d83d70a7a69eabfc58b78fa3e47fee56d2da Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 16:39:48 +0500 Subject: [PATCH 056/165] Updated on 2026-08-14 --- core/res/src/main/res/values-es/strings.xml | 4 +-- core/res/src/main/res/values/strings.xml | 1 + .../src/main/res/drawable/ic_warning_16.xml | 14 +++++++++ .../v2/impl/amount/entity/SwapAmountUM.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 3 ++ .../SwapAmountPrimaryReadyStateTransformer.kt | 1 + ...wapAmountSecondaryReadyStateTransformer.kt | 1 + .../SwapAmountSelectQuoteTransformer.kt | 3 ++ .../SwapAmountSetQuotesTransformer.kt | 4 +++ .../impl/amount/ui/SwapAmountBlockContent.kt | 1 + .../ui/preview/SwapAmountContentPreview.kt | 2 ++ .../ui/SwapChooseProviderContent.kt | 31 +++++++++++++++++++ 12 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_warning_16.xml diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 6f67b2c033..d075e7cd0c 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1268,8 +1268,8 @@ Actualmente, MATIC está migrando a POL. Sin embargo, no se ha fijado ninguna fecha límite y MATIC aún no está obsoleto. Puede seguir usando el token MATIC de forma segura o utilizar intercambios para cambiarlo por POL. Migración de MATIC a POL - Use su tarjeta o anillo para obtener una dirección para la red - Use su tarjeta o anillo para obtener direcciónes para la red + Use su tarjeta o anillo para obtener una dirección para la red %d + Use su tarjeta o anillo para obtener direcciónes para las redes %d Faltan algunas direcciones La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 039b9d2cdb..37c1670198 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -400,6 +400,7 @@ Provider Best rate FCA Warning List + Provider in FCA warning list Available up to %s Available from %s Unavailable for this pair diff --git a/core/ui/src/main/res/drawable/ic_warning_16.xml b/core/ui/src/main/res/drawable/ic_warning_16.xml new file mode 100644 index 0000000000..ff30f1ba7e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_warning_16.xml @@ -0,0 +1,14 @@ + + + + diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 4c077efd90..147e318a85 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -48,6 +48,7 @@ internal sealed class SwapAmountUM { val swapCurrencies: SwapCurrencies, val swapQuotes: ImmutableList, val selectedQuote: SwapQuoteUM, + val showFCAWarning: Boolean, // extra data val appCurrency: AppCurrency?, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index efc0561da2..62e938cd22 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -24,6 +24,7 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.models.SwapQuoteModel @@ -150,6 +151,7 @@ internal class SwapAmountModel @Inject constructor( quoteUM = quoteUM, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), ), ) } @@ -533,6 +535,7 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, + needApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index f6c07b51ce..3279e896ab 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -54,6 +54,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, showBestRateAnimation = showBestRateAnimation, + showFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 729519512d..7a0a94e535 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -53,6 +53,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, showBestRateAnimation = showBestRateAnimation, + showFCAWarning = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 7213bcb3c5..cd0df36880 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer @@ -18,6 +19,7 @@ internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, + private val needApplyFCARestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -29,6 +31,7 @@ internal class SwapAmountSelectQuoteTransformer( return prevState.copy( isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, selectedQuote = quoteUM, + showFCAWarning = needApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content val amountField = swapAmountField?.amountField as? AmountState.Data diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index a3f6263d18..b9d220ba0b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -8,6 +8,7 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent +import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive import com.tangem.utils.transformer.Transformer @@ -20,6 +21,7 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, + private val needApplyFcaRestrictions: Boolean, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -36,6 +38,8 @@ internal class SwapAmountSetQuotesTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + needApplyFCARestrictions = needApplyFcaRestrictions && + selectedQuote.provider?.isRestrictedByFCA() == true, ) val updatedState = selectQuoteTransformer.transform(prevState = prevState) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index e2a6522a74..f68cfcadc4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -114,6 +114,7 @@ internal fun SwapAmountBlockContent( start.linkTo(parent.start) end.linkTo(parent.end) }, + showFCAWarning = amountUM.showFCAWarning, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index df8d9da742..627af48d84 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -89,6 +89,7 @@ internal data object SwapAmountContentPreview { swapRateType = ExpressRateType.Float, appCurrency = AppCurrency.Default, showBestRateAnimation = false, + showFCAWarning = false, ) val defaultState = SwapAmountUM.Content( @@ -126,5 +127,6 @@ internal data object SwapAmountContentPreview { swapRateType = ExpressRateType.Float, isPrimaryButtonEnabled = true, showBestRateAnimation = false, + showFCAWarning = true, ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 2ef78b6506..0e00951292 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -33,6 +34,7 @@ import androidx.constraintlayout.compose.Visibility import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette @@ -44,6 +46,7 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.R import kotlinx.coroutines.delay +@Suppress("LongParameterList") @Composable fun SwapChooseProviderContent( expressProvider: ExpressProvider?, @@ -51,6 +54,7 @@ fun SwapChooseProviderContent( showBestRateAnimation: Boolean, onClick: () -> Unit, onFinishAnimation: () -> Unit, + showFCAWarning: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -83,6 +87,32 @@ fun SwapChooseProviderContent( SpacerWMax() ProviderInfo(expressProvider, isBestRate, showBestRateAnimation, onFinishAnimation) } + if (showFCAWarning) { + FcaProviderWarning( + modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 12.dp), + ) + } + } +} + +@Composable +private fun FcaProviderWarning(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_warning_16), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerW8() + Text( + text = stringResourceSafe(R.string.express_provider_in_fca_warning_list), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -281,6 +311,7 @@ private fun SwapChooseProviderContent_Preview() { slippage = null, ), onClick = {}, + showFCAWarning = true, onFinishAnimation = {}, ) } From 6178034e714a6730329fa34b83ea8d99a853a45e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 20:04:35 +0500 Subject: [PATCH 057/165] Updated on 2026-08-14 --- .../common/ui/userwallet/UserWalletItem.kt | 19 +++- .../converter/UserWalletItemUMConverter.kt | 11 ++ .../ui/userwallet/state/UserWalletItemUM.kt | 2 + .../core/ui/components/block/BlockItem.kt | 4 + .../core/ui/components/block/model/BlockUM.kt | 2 + .../tangem/core/ui/components/label/Label.kt | 104 +++++++++++++++++ .../ui/components/label/entity/LabelUM.kt | 12 ++ .../port/entity/AddExistingWalletImportUM.kt | 6 +- .../model/AddExistingWalletImportModel.kt | 4 +- .../model/ImportSeedPhraseUiStateBuilder.kt | 16 +-- .../port/ui/AddExistingWalletImportContent.kt | 12 +- .../hotwallet/common/ui/OptionBlock.kt | 2 +- .../check/model/ManualBackupCheckModel.kt | 3 + .../walletbackup/entity/WalletBackupUM.kt | 6 +- .../walletbackup/model/WalletBackupModel.kt | 48 ++++++-- .../walletbackup/ui/WalletBackupContent.kt | 88 +++++--------- .../wallet-settings/impl/build.gradle.kts | 3 + .../preview/PreviewWalletSettingsComponent.kt | 12 +- .../model/WalletSettingsModel.kt | 100 +++++++++------- .../walletsettings/utils/ItemsBuilder.kt | 107 ++++++++++++++---- 20 files changed, 401 insertions(+), 160 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 704a867b47..97a5063ec8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -30,6 +30,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -67,8 +70,10 @@ fun UserWalletItem( balance = state.balance, ) + state.label?.let { Label(it) } + when (state.endIcon) { - UserWalletItemUM.EndIcon.None -> {} + UserWalletItemUM.EndIcon.None -> Unit UserWalletItemUM.EndIcon.Arrow -> { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24), @@ -257,6 +262,18 @@ private fun Preview_UserWalletItem( private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( + UserWalletItemUM( + id = UserWalletId("user_wallet_0".encodeToByteArray()), + name = stringReference("Mobile Wallet"), + information = getInformation(cardCount = 1), + balance = UserWalletItemUM.Balance.Locked, + label = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + isEnabled = true, + onClick = {}, + ), UserWalletItemUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), name = stringReference("My Wallet"), diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 7244b4d23a..29c3f57b8a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -2,7 +2,10 @@ package com.tangem.common.ui.userwallet.converter import com.tangem.common.ui.R import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM 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.core.ui.format.bigdecimal.fiat @@ -51,6 +54,14 @@ class UserWalletItemUMConverter( imageState = artwork?.let { UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it)) } ?: UserWalletItemUM.ImageState.Loading, + label = if (this is UserWallet.Hot && !this.backedUp) { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + } else { + null + }, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index 377ce963f1..e9f98863b8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -1,6 +1,7 @@ package com.tangem.common.ui.userwallet.state import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId import javax.annotation.concurrent.Immutable @@ -15,6 +16,7 @@ data class UserWalletItemUM( val isEnabled: Boolean, val endIcon: EndIcon = EndIcon.None, val onClick: () -> Unit, + val label: LabelUM? = null, ) { enum class EndIcon { None, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt index d729408a97..8aab6c9598 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.components.label.Label import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -38,6 +39,7 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { ) Text( + modifier = Modifier.weight(1f), text = model.text.resolveReference(), style = TangemTheme.typography.subtitle1, color = when (model.accentType) { @@ -48,6 +50,8 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { maxLines = 1, overflow = TextOverflow.Ellipsis, ) + + model.label?.let { Label(it) } } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt index df7e04af33..c950b5e4e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.block.model import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference data class BlockUM( @@ -8,6 +9,7 @@ data class BlockUM( @DrawableRes val iconRes: Int, val onClick: () -> Unit, val accentType: AccentType = AccentType.NONE, + val label: LabelUM? = null, ) { enum class AccentType { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt new file mode 100644 index 0000000000..7e95a24776 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/Label.kt @@ -0,0 +1,104 @@ +package com.tangem.core.ui.components.label + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Label component + * + * @param state component state + * @param modifier composable modifier + * + * @see Figma + */ +@Composable +fun Label(state: LabelUM, modifier: Modifier = Modifier) { + val backgroundColor by animateColorAsState( + targetValue = when (state.style) { + LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) + LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked + LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) + }, + ) + + val textColor by animateColorAsState( + targetValue = when (state.style) { + LabelStyle.ACCENT -> TangemTheme.colors.text.accent + LabelStyle.REGULAR -> TangemTheme.colors.text.secondary + LabelStyle.WARNING -> TangemTheme.colors.text.warning + }, + ) + + AnimatedContent(targetState = state.text) { text -> + Box( + modifier = modifier + .padding(horizontal = 4.dp) + .background( + color = backgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + ) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption1, + color = textColor, + ) + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LabelPreview() { + TangemThemePreview { + Column( + modifier = Modifier.padding(16.dp), + ) { + Label( + state = LabelUM( + text = TextReference.Str("Regular Label"), + style = LabelStyle.REGULAR, + ), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Label( + state = LabelUM( + text = TextReference.Str("Accent Label"), + style = LabelStyle.ACCENT, + ), + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Label( + state = LabelUM( + text = TextReference.Str("Warning Label"), + style = LabelStyle.WARNING, + ), + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt new file mode 100644 index 0000000000..70b134bfe8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/label/entity/LabelUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.components.label.entity + +import com.tangem.core.ui.extensions.TextReference + +data class LabelUM( + val text: TextReference, + val style: LabelStyle, +) + +enum class LabelStyle { + REGULAR, ACCENT, WARNING, +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt index 420bd92d31..667d538a14 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -13,9 +13,9 @@ internal data class AddExistingWalletImportUM( val onPassphraseInfoClick: () -> Unit, val wordsErrorText: TextReference?, val invalidWords: ImmutableList, - val createWalletEnabled: Boolean, - val createWalletProgress: Boolean, - val createWalletClick: () -> Unit, + val importWalletEnabled: Boolean, + val importWalletProgress: Boolean, + val importWalletClick: () -> Unit, val suggestionsList: ImmutableList, val onSuggestionClick: (String) -> Unit, val infoBottomSheetConfig: TangemBottomSheetConfig, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index f7664bfa55..f11d60b7bc 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -55,7 +55,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { modelScope.launch { uiState.update { - it.copy(createWalletProgress = true) + it.copy(importWalletProgress = true) } runCatching { @@ -68,7 +68,7 @@ internal class AddExistingWalletImportModel @Inject constructor( Timber.e(it) uiState.update { - it.copy(createWalletProgress = false) + it.copy(importWalletProgress = false) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index bc05c78e98..b09bdd3265 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -35,8 +35,8 @@ internal class ImportSeedPhraseUiStateBuilder( passPhrase = TextFieldValue(""), wordsErrorText = null, invalidWords = persistentListOf(), - createWalletEnabled = false, - createWalletProgress = false, + importWalletEnabled = false, + importWalletProgress = false, suggestionsList = persistentListOf(), wordsChange = { launchInterceptWords(wordsField = it) @@ -50,7 +50,7 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { state -> state.copy(passPhrase = it) } }, onPassphraseInfoClick = ::showInfoBS, - createWalletClick = ::onCreateWallet, + importWalletClick = ::onCreateWallet, onSuggestionClick = { word -> addSuggestedWord(word) }, readyToImport = false, infoBottomSheetConfig = TangemBottomSheetConfig.Empty, @@ -115,7 +115,7 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { it.copy( - createWalletEnabled = false, + importWalletEnabled = false, wordsErrorText = null, ) } @@ -130,7 +130,7 @@ internal class ImportSeedPhraseUiStateBuilder( it.copy( invalidWords = invalidWords.toImmutableList(), wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words), - createWalletEnabled = false, + importWalletEnabled = false, ) } return @@ -143,7 +143,7 @@ internal class ImportSeedPhraseUiStateBuilder( it.copy( invalidWords = emptyList().toImmutableList(), wordsErrorText = null, - createWalletEnabled = true, + importWalletEnabled = true, ) } readyToImport(true) @@ -154,13 +154,13 @@ internal class ImportSeedPhraseUiStateBuilder( updateUiState { it.copy( wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum), - createWalletEnabled = false, + importWalletEnabled = false, ) } } else { updateUiState { it.copy( - createWalletEnabled = false, + importWalletEnabled = false, wordsErrorText = null, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 33e7872bb8..9d1d73601c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -96,9 +96,9 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo .padding(16.dp) .fillMaxWidth(), text = stringResourceSafe(id = R.string.common_import), - enabled = state.createWalletEnabled, - showProgress = state.createWalletProgress, - onClick = state.createWalletClick, + enabled = state.importWalletEnabled, + showProgress = state.importWalletProgress, + onClick = state.importWalletClick, ) } @@ -228,9 +228,9 @@ private fun PreviewAddExistingWalletImportContent() { onPassphraseInfoClick = {}, wordsErrorText = null, invalidWords = persistentListOf(), - createWalletEnabled = false, - createWalletProgress = false, - createWalletClick = {}, + importWalletEnabled = false, + importWalletProgress = false, + importWalletClick = {}, suggestionsList = persistentListOf(), onSuggestionClick = {}, infoBottomSheetConfig = TangemBottomSheetConfig.Empty, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index ad1d8637e1..8e90402e09 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme -internal const val DISABLED_COLORS_ALPHA = 0.5f +private const val DISABLED_COLORS_ALPHA = 0.5f @Suppress("LongParameterList") @Composable diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt index 6d62bc73c9..e0778baa69 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -111,6 +111,9 @@ internal class ManualBackupCheckModel @Inject constructor( } callbacks.onCompleteClick() } + uiState.update { + it.copy(completeButtonProgress = false) + } }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index be22d8cd38..5df45d62a0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.hotwallet.walletbackup.entity +import com.tangem.core.ui.components.label.entity.LabelUM + internal data class WalletBackupUM( val onBackClick: () -> Unit, - val recoveryPhraseStatus: BackupStatus, - val googleDriveStatus: BackupStatus, + val recoveryPhraseStatus: LabelUM?, + val googleDriveStatus: LabelUM?, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index f85e281c17..18e601c406 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -4,9 +4,13 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.R +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.WalletBackupComponent -import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -26,8 +30,14 @@ internal class WalletBackupModel @Inject constructor( field = MutableStateFlow( WalletBackupUM( onBackClick = { router.pop() }, - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), onRecoveryPhraseClick = { }, onGoogleDriveClick = { }, ), @@ -37,18 +47,38 @@ internal class WalletBackupModel @Inject constructor( getWalletUseCase.invokeFlow(params.userWalletId) .map { it.getOrNull() } .distinctUntilChanged() + .filterNotNull() .onEach { - updateBackupStatuses() + updateBackupStatuses(it) } .launchIn(modelScope) } - private fun updateBackupStatuses() { + private fun updateBackupStatuses(userWallet: UserWallet) { uiState.update { currentState -> - currentState.copy( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, - ) + if (userWallet is UserWallet.Hot) { + currentState.updateBackupStatusesHotWallet(userWallet) + } else { + currentState + } } } + + private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy( + recoveryPhraseStatus = if (userWallet.backedUp) { + LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ) + } else { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + }, + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), + ) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 7d5ac986a4..66e0c92d43 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -1,20 +1,14 @@ package com.tangem.features.hotwallet.walletbackup.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -28,7 +22,10 @@ import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.features.hotwallet.common.ui.OptionBlock import com.tangem.core.ui.R -import com.tangem.features.hotwallet.common.ui.DISABLED_COLORS_ALPHA +import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.resourceReference @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -54,7 +51,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_seed_title), description = stringResourceSafe(R.string.hw_backup_seed_description), badge = { - BackupStatusBadge(status = state.recoveryPhraseStatus) + state.recoveryPhraseStatus?.let { Label(it) } }, onClick = state.onRecoveryPhraseClick, enabled = true, @@ -66,7 +63,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_google_drive_title), description = stringResourceSafe(R.string.hw_backup_google_drive_description), badge = { - BackupStatusBadge(status = state.googleDriveStatus) + state.googleDriveStatus?.let { Label(it) } }, onClick = state.onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, @@ -76,49 +73,6 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod } } -@Composable -private fun BackupStatusBadge(status: BackupStatus, modifier: Modifier = Modifier) { - val text = when (status) { - BackupStatus.Done -> stringResourceSafe(R.string.common_done) - BackupStatus.ComingSoon -> stringResourceSafe(R.string.common_coming_soon) - BackupStatus.NoBackup -> stringResourceSafe(R.string.hw_backup_no_backup) - } - - val backgroundColor by animateColorAsState( - targetValue = when (status) { - BackupStatus.Done -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) - BackupStatus.ComingSoon -> TangemTheme.colors.control.unchecked.copy(DISABLED_COLORS_ALPHA) - BackupStatus.NoBackup -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) - }, - ) - - val textColor by animateColorAsState( - targetValue = when (status) { - BackupStatus.Done -> TangemTheme.colors.text.accent - BackupStatus.ComingSoon -> TangemTheme.colors.text.secondary.copy(DISABLED_COLORS_ALPHA) - BackupStatus.NoBackup -> TangemTheme.colors.text.warning - }, - ) - - AnimatedContent(targetState = text) { text -> - Box( - modifier = modifier - .padding(horizontal = 4.dp) - .background( - color = backgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - ) { - Text( - text = text, - style = TangemTheme.typography.caption1, - color = textColor, - ) - } - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -131,22 +85,40 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider: private class WalletBackupUMProvider : CollectionPreviewParameterProvider( collection = listOf( WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.ComingSoon, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_coming_soon), + style = LabelStyle.REGULAR, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.NoBackup, - googleDriveStatus = BackupStatus.NoBackup, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, ), WalletBackupUM( - recoveryPhraseStatus = BackupStatus.Done, - googleDriveStatus = BackupStatus.Done, + recoveryPhraseStatus = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), + googleDriveStatus = LabelUM( + text = resourceReference(R.string.common_done), + style = LabelStyle.ACCENT, + ), onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index f327a964d5..b69105d2c8 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -65,4 +65,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.timber) implementation(deps.reKotlin) + + /** Tangem libraries */ + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 023afb9685..453120249f 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -4,11 +4,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.hot.sdk.model.HotWalletId internal class PreviewWalletSettingsComponent : WalletSettingsComponent { @@ -18,7 +20,13 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { router = DummyRouter(), analyticsEventHandler = DummyAnalyticsEventHandler(), ).buildItems( - userWalletId = UserWalletId("011"), + userWallet = UserWallet.Hot( + walletId = UserWalletId("011"), + name = "My Wallet", + hotWalletId = HotWalletId("", HotWalletId.AuthType.NoPassword), + wallets = null, + backedUp = false, + ), userWalletName = "My Wallet", isReferralAvailable = true, isLinkMoreCardsAvailable = true, @@ -36,7 +44,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onCheckedNotificationsChanged = {}, onNotificationsDescriptionClick = {}, isNotificationsPermissionGranted = false, - isHotWalletEnabled = false, + onAccessCodeClick = {}, ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 7f8da42be9..061c68bb82 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -31,8 +31,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.usecase.* import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents @@ -43,7 +41,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.ItemsBuilder -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList @@ -75,7 +72,6 @@ internal class WalletSettingsModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val settingsManager: SettingsManager, private val permissionsRepository: PermissionRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val notificationsRepository: NotificationsRepository, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, ) : Model() { @@ -100,7 +96,6 @@ internal class WalletSettingsModel @Inject constructor( getWalletNotificationsEnabledUseCase(params.userWalletId), ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine - wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] [Hot Wallet] Wallet Settings val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() @@ -114,7 +109,6 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), - isHotWalletEnabled = hotWalletFeatureToggles.isHotWalletEnabled, ), ) } @@ -133,51 +127,65 @@ internal class WalletSettingsModel @Inject constructor( } private fun buildItems( - userWallet: UserWallet.Cold, + userWallet: UserWallet, dialogNavigation: SlotNavigation, isRenameWalletAvailable: Boolean, isNFTEnabled: Boolean, isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, isNotificationsPermissionGranted: Boolean, - isHotWalletEnabled: Boolean, - ): PersistentList = itemsBuilder.buildItems( - userWalletId = userWallet.walletId, - userWalletName = userWallet.name, - isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), - isLinkMoreCardsAvailable = userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, - isManageTokensAvailable = userWallet.isMultiCurrency, - isRenameWalletAvailable = isRenameWalletAvailable, - renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, - isNFTFeatureEnabled = userWallet.isMultiCurrency, - isNFTEnabled = isNFTEnabled, - onCheckedNFTChange = ::onCheckedNFTChange, - forgetWallet = { - val message = DialogMessage( - message = resourceReference(R.string.user_wallet_list_delete_prompt), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.common_delete), - warning = true, - onClick = ::forgetWallet, - ) - }, - secondActionBuilder = { cancelAction() }, - ) + ): PersistentList { + val isMultiCurrency = when (userWallet) { + is UserWallet.Cold -> userWallet.isMultiCurrency + is UserWallet.Hot -> true + } + return itemsBuilder.buildItems( + userWallet = userWallet, + userWalletName = userWallet.name, + isReferralAvailable = when (userWallet) { + is UserWallet.Cold -> userWallet.cardTypesResolver.isTangemWallet() + is UserWallet.Hot -> false + }, + isLinkMoreCardsAvailable = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup + is UserWallet.Hot -> false + }, + isManageTokensAvailable = isMultiCurrency, + isRenameWalletAvailable = isRenameWalletAvailable, + renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, + isNFTFeatureEnabled = isMultiCurrency, + isNFTEnabled = isNFTEnabled, + onCheckedNFTChange = ::onCheckedNFTChange, + forgetWallet = { + val message = DialogMessage( + message = resourceReference(R.string.user_wallet_list_delete_prompt), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_delete), + warning = true, + onClick = ::forgetWallet, + ) + }, + secondActionBuilder = { cancelAction() }, + ) - messageSender.send(message) - }, - onLinkMoreCardsClick = { - onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) - }, - onReferralClick = { onReferralClick(userWallet) }, - isNotificationsEnabled = isNotificationsEnabled, - isNotificationsFeatureEnabled = isNotificationsFeatureEnabled, - isNotificationsPermissionGranted = isNotificationsPermissionGranted, - onCheckedNotificationsChanged = ::onCheckedNotificationsChange, - onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, - isHotWalletEnabled = isHotWalletEnabled, - ) + messageSender.send(message) + }, + onLinkMoreCardsClick = { + when (userWallet) { + is UserWallet.Cold -> onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) + is UserWallet.Hot -> Unit + } + }, + onReferralClick = { onReferralClick(userWallet) }, + isNotificationsEnabled = isNotificationsEnabled, + isNotificationsFeatureEnabled = isNotificationsFeatureEnabled, + isNotificationsPermissionGranted = isNotificationsPermissionGranted, + onCheckedNotificationsChanged = ::onCheckedNotificationsChange, + onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, + onAccessCodeClick = ::onAccessCodeClick, + ) + } private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { val config = DialogConfig.RenameWallet( @@ -320,4 +328,8 @@ internal class WalletSettingsModel @Inject constructor( router.push(AppRoute.ReferralProgram(userWallet.walletId)) } } + + private fun onAccessCodeClick() { + // TODO [REDACTED_TASK_KEY] + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 1349f34906..5086f22579 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -6,12 +6,15 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.hot.sdk.model.HotWalletId import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -25,7 +28,7 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") fun buildItems( - userWalletId: UserWalletId, + userWallet: UserWallet, userWalletName: String, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, @@ -43,25 +46,18 @@ internal class ItemsBuilder @Inject constructor( renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, - isHotWalletEnabled: Boolean, + onAccessCodeClick: () -> Unit, ): PersistentList = persistentListOf() .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) - .run { - if (isNFTFeatureEnabled) { - add(buildNFTItem(isNFTEnabled, onCheckedNFTChange)) - } else { - this - } - } + .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( - userWalletId = userWalletId, + userWallet = userWallet, isLinkMoreCardsAvailable = isLinkMoreCardsAvailable, isReferralAvailable = isReferralAvailable, isManageTokensAvailable = isManageTokensAvailable, onLinkMoreCardsClick = onLinkMoreCardsClick, onReferralClick = onReferralClick, - isHotWalletEnabled = isHotWalletEnabled, ), ) .addAll( @@ -73,8 +69,27 @@ internal class ItemsBuilder @Inject constructor( onNotificationsDescriptionClick = onNotificationsDescriptionClick, ), ) + .addAll( + buildNFTItems( + isNFTFeatureEnabled = isNFTFeatureEnabled, + isNFTEnabled = isNFTEnabled, + onCheckedNFTChange = onCheckedNFTChange, + ), + ) .add(buildForgetItem(forgetWallet)) + private fun buildNFTItems( + isNFTFeatureEnabled: Boolean, + isNFTEnabled: Boolean, + onCheckedNFTChange: (Boolean) -> Unit, + ): List { + return if (isNFTFeatureEnabled) { + listOf(buildNFTItem(isNFTEnabled, onCheckedNFTChange)) + } else { + emptyList() + } + } + private fun buildNotificationItems( isNotificationsFeatureEnabled: Boolean, isNotificationsPermissionGranted: Boolean, @@ -133,17 +148,35 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") private fun buildCardItem( - userWalletId: UserWalletId, + userWallet: UserWallet, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, - isHotWalletEnabled: Boolean, ) = WalletSettingsItemUM.WithItems( id = "card", description = resourceReference(R.string.settings_card_settings_footer), blocks = buildList { + val userWalletId = userWallet.walletId + val isHotWallet = userWallet is UserWallet.Hot + if (isHotWallet) { + val hasBackup = userWallet.backedUp + BlockUM( + text = resourceReference(R.string.common_backup), + iconRes = R.drawable.ic_more_cards_24, + onClick = { router.push(AppRoute.WalletBackup(userWalletId)) }, + label = if (hasBackup) { + null + } else { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + }, + ).let(::add) + } + if (isManageTokensAvailable) { BlockUM( text = resourceReference(R.string.add_tokens_title), @@ -163,17 +196,11 @@ internal class ItemsBuilder @Inject constructor( ).let(::add) } - BlockUM( - text = resourceReference(R.string.card_settings_title), - iconRes = R.drawable.ic_card_settings_24, - onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, - ).let(::add) - - if (isHotWalletEnabled) { + if (!isHotWallet) { BlockUM( - text = resourceReference(R.string.common_backup), - iconRes = R.drawable.ic_more_cards_24, - onClick = { router.push(AppRoute.WalletBackup(userWalletId)) }, + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, ).let(::add) } @@ -199,4 +226,36 @@ internal class ItemsBuilder @Inject constructor( ), ), ) + + private fun buildAccessCodeItem(userWallet: UserWallet, onItemClick: () -> Unit): List { + return when (userWallet) { + is UserWallet.Cold -> emptyList() + is UserWallet.Hot -> buildHotWalletAccessCodeItem(userWallet, onItemClick) + } + } + + private fun buildHotWalletAccessCodeItem( + userWallet: UserWallet.Hot, + onItemClick: () -> Unit, + ): List { + val isCodeSet = userWallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + return listOf( + WalletSettingsItemUM.WithItems( + id = "access_code", + description = resourceReference(R.string.wallet_settings_access_code_description), + blocks = persistentListOf( + BlockUM( + text = if (isCodeSet) { + resourceReference(R.string.wallet_settings_change_access_code_title) + } else { + resourceReference(R.string.wallet_settings_set_access_code_title) + }, + iconRes = R.drawable.ic_lock_24, + onClick = onItemClick, + accentType = BlockUM.AccentType.ACCENT, + ), + ), + ), + ) + } } \ No newline at end of file From 0cb35b7ab0d9639a9fa9977c573753b61793fe79 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 15:20:58 +0000 Subject: [PATCH 058/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..78d485789e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1129" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-446" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From a30eeb4f5d57b1a3c7133ae0c8a7e1e213914fcb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 12:36:38 +0300 Subject: [PATCH 059/165] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 9 +- .../screens/ReferralProgramPageObject.kt | 84 +++++++++++++++++++ .../kotlin/com/tangem/tests/DetailsTest.kt | 47 +++++++++++ .../components/buttons/common/TangemButton.kt | 8 +- .../tangem/core/ui/test/BaseButtonTestTags.kt | 2 + .../ui/test/ReferralProgramScreenTestTags.kt | 8 ++ .../feature/referral/ui/ReferralScreen.kt | 11 ++- 7 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 57c7c6cd46..823d7df27c 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -80,9 +80,12 @@ abstract class BaseTestCase : TestCase( ) = before { hiltRule.inject() runBlocking { - appPreferencesStore.editData { mutablePreferences -> mutablePreferences.set( - key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, value = false - ) } + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.set( + key = PreferencesKeys.NOTIFICATIONS_USER_ALLOW_SEND_ADDRESSES_KEY, + value = false + ) + } } apiEnvironmentRule.setup(apiConfigsManager) ActivityScenario.launch(MainActivity::class.java) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt new file mode 100644 index 0000000000..b16bba6546 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ReferralProgramPageObject.kt @@ -0,0 +1,84 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.ReferralProgramScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.feature.referral.presentation.R as ReferralPresentationR +import androidx.compose.ui.test.hasText as withText + +class ReferralProgramPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.details_referral_title)) + useUnmergedTree = true + } + + val referTitle: KNode = child { + hasText(getResourceString(R.string.referral_title)) + useUnmergedTree = true + } + + val image: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.IMAGE) + useUnmergedTree = true + } + + val infoForYouText: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT) + useUnmergedTree = true + } + + val infoForYouBlock: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK) + hasAnyDescendant( + withText( + getResourceString(ReferralPresentationR.string.referral_point_currencies_title), + substring = true + ) + ) + useUnmergedTree = true + } + + val infoForYourFriendText: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.INFO_FOR_YOUR_FRIEND_TEXT) + useUnmergedTree = true + } + + val infoForYourFriendBlock: KNode = child { + hasTestTag(ReferralProgramScreenTestTags.CONDITION_BLOCK) + hasAnyDescendant( + withText( + getResourceString(ReferralPresentationR.string.referral_point_discount_title), + substring = true + ) + ) + useUnmergedTree = true + } + + val agreementText: KNode = child { + hasText(getResourceString( + ReferralPresentationR.string.referral_tos_not_enroled_prefix) + " " + + getResourceString(ReferralPresentationR.string.common_terms_and_conditions ) + " " + + getResourceString(ReferralPresentationR.string.referral_tos_suffix), + substring = true + ) + useUnmergedTree = true + } + + val participateButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onReferralProgramScreen(function: ReferralProgramPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 805c4bfe72..df318a9456 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -5,9 +5,12 @@ import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.onDetailsScreen +import com.tangem.screens.onReferralProgramScreen import com.tangem.screens.onTopBar import com.tangem.screens.onWalletSettingsScreen import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName import org.junit.Test @HiltAndroidTest @@ -153,4 +156,48 @@ class DetailsTest : BaseTestCase() { } } } + + @AllureId("3647") + @DisplayName("Referral program: validate screen") + @Test + fun validateReferralProgramScreenTest() = + setupHooks().run { + scenario(OpenMainScreenScenario(composeTestRule)) + step("Open wallet details") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Open 'Wallet settings' screen") { + onDetailsScreen { walletNameButton.clickWithAssertion() } + } + step("Click on 'Referral program' button ") { + onWalletSettingsScreen { referralProgramButton.clickWithAssertion() } + } + step("Assert 'Referral program' screen title is displayed") { + onReferralProgramScreen { title.assertIsDisplayed() } + } + step("Assert 'Referral program' screen image is displayed") { + onReferralProgramScreen { image.assertIsDisplayed() } + } + step("Assert 'Referral program' screen refer title is displayed") { + onReferralProgramScreen { referTitle.assertIsDisplayed() } + } + step("Assert info for you title is displayed") { + onReferralProgramScreen { infoForYouText.assertIsDisplayed() } + } + step("Assert info for you text is displayed") { + onReferralProgramScreen { infoForYouBlock.assertIsDisplayed() } + } + step("Assert info for your friend title is displayed") { + onReferralProgramScreen { infoForYourFriendText.assertIsDisplayed() } + } + step("Assert info for your friend text is displayed") { + onReferralProgramScreen { infoForYourFriendBlock.assertIsDisplayed() } + } + step("Assert agreement text is displayed") { + onReferralProgramScreen { agreementText.assertIsDisplayed() } + } + step("Assert 'Participate' button is displayed") { + onReferralProgramScreen { participateButton.assertIsDisplayed() } + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index 90dbd2db78..aa6f681bce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -28,7 +28,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.utils.MultipleClickPreventer -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LongMethod") @Composable fun TangemButton( text: String, @@ -78,7 +78,8 @@ fun TangemButton( ResizableText( modifier = Modifier .weight(1f, fill = false) - .heightIn(MinButtonContentSize, maxContentSize), + .heightIn(MinButtonContentSize, maxContentSize) + .testTag(BaseButtonTestTags.TEXT), text = text, style = textStyle, color = colors.contentColor(enabled = enabled).value, @@ -92,7 +93,8 @@ fun TangemButton( Icon( modifier = Modifier .buttonContentSize(maxContentSize) - .padding(vertical = 2.dp), + .padding(vertical = 2.dp) + .testTag(BaseButtonTestTags.ICON), painter = painterResource(id = iconResId), tint = colors.contentColor(enabled = enabled).value, contentDescription = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt index 9d4954ba04..03bfd8f0d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseButtonTestTags.kt @@ -2,4 +2,6 @@ package com.tangem.core.ui.test object BaseButtonTestTags { const val BUTTON = "BASE_BUTTON" + const val ICON = "BASE_BUTTON_ICON" + const val TEXT = "BASE_BUTTON_TEXT" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt new file mode 100644 index 0000000000..55a20fabe0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ReferralProgramScreenTestTags.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.test + +object ReferralProgramScreenTestTags { + const val IMAGE = "REFERRAL_PROGRAM_SCREEN_IMAGE" + const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK" + const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT" + const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT" +} \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 5bf12360cb..4aae138a4f 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -34,6 +35,7 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.ReferralProgramScreenTestTags import com.tangem.feature.referral.domain.models.ExpectedAward import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException @@ -138,7 +140,8 @@ private fun Header() { modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing32) .fillMaxWidth() - .height(TangemTheme.dimens.size200), + .height(TangemTheme.dimens.size200) + .testTag(ReferralProgramScreenTestTags.IMAGE), ) SpacerH24() Text( @@ -233,7 +236,9 @@ private fun LoadingCondition(@DrawableRes iconResId: Int) { @Composable private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> Unit) { Row( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .testTag(ReferralProgramScreenTestTags.CONDITION_BLOCK), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), verticalAlignment = Alignment.Top, ) { @@ -268,6 +273,7 @@ private fun InfoForYou(award: String, networkName: String, address: String? = nu ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOU_TEXT), ) } } @@ -332,6 +338,7 @@ private fun ConditionInfo(title: String, subtitleContent: @Composable () -> Unit text = title, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, + modifier = Modifier.testTag(ReferralProgramScreenTestTags.INFO_FOR_YOUR_FRIEND_TEXT), ) subtitleContent() } From 077b2e9ecf27bab1bffb719285fda8c7b949edf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 15:19:51 +0500 Subject: [PATCH 060/165] Updated on 2026-08-14 --- .../features/send/v2/send/confirm/model/SendConfirmModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 343cd6d29e..a458d480c5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -565,7 +565,7 @@ internal class SendConfirmModel @Inject constructor( state.copy( navigationUM = NavigationUM.Content( title = if (state.isRedesignEnabled) { - stringReference("") + resourceReference(id = R.string.common_send) } else { resourceReference( id = R.string.send_summary_title, From 34f07ed52d1be4d82fa9cc57160d5de191f4ca19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 18:05:37 +0400 Subject: [PATCH 061/165] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 4 +- .../domain/account/models/AccountList.kt | 30 +- .../usecase/AddCryptoPortfolioUseCase.kt | 101 +++++- .../domain/account/models/AccountListTest.kt | 284 +++++++++++++++- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 317 ++++++++++++++++++ .../tangem/domain/models/account/Account.kt | 21 ++ .../domain/models/account/AccountName.kt | 5 + .../domain/models/account/AccountNameTest.kt | 11 + .../domain/models/account/AccountTest.kt | 39 ++- 9 files changed, 781 insertions(+), 31 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 5c303041a0..c4f9660136 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -17,8 +17,8 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideAddCryptoPortfolioUseCase(): AddCryptoPortfolioUseCase { - return AddCryptoPortfolioUseCase() + fun provideAddCryptoPortfolioUseCase(accountsCRUDRepository: AccountsCRUDRepository): AddCryptoPortfolioUseCase { + return AddCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index e782ece215..670a7b2bb6 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -28,6 +28,10 @@ data class AccountList private constructor( val mainAccount: Account.CryptoPortfolio get() = accounts.first { it is Account.CryptoPortfolio && it.isMainAccount } as Account.CryptoPortfolio + /** Returns true if more accounts can be added (the maximum number of accounts has not been reached) */ + val canAddMoreAccounts: Boolean + get() = accounts.size < MAX_ACCOUNTS_COUNT + /** * Adds an account to the account list. * If an account with the same identifier already exists, it will be replaced. @@ -55,12 +59,15 @@ data class AccountList private constructor( * @param other the account to remove */ operator fun minus(other: Account): Either { + val isExistingAccount = this.accounts.any { it.accountId == other.accountId } + val accounts = this.accounts.toMutableSet().apply { + removeIf { it.accountId == other.accountId } + } + return invoke( userWallet = this.userWallet, - accounts = this.accounts.toMutableSet().apply { - removeIf { it.accountId == other.accountId } - }, - totalAccounts = this.totalAccounts - 1, + accounts = accounts, + totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, ) } @@ -140,6 +147,21 @@ data class AccountList private constructor( AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) } + /** + * Factory method to create an empty [AccountList] with a main crypto portfolio account + * + * @param userWallet the user wallet associated with the account list + */ + fun createEmpty(userWallet: UserWallet): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf( + Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + ), + totalAccounts = 1, + ) + } + private fun Set.mainAccountsCount(): Int { return count { (it as? Account.CryptoPortfolio)?.isMainAccount == true } } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index fb2ebb4717..4e68e70e3a 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -1,9 +1,13 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.Option +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.account.models.AccountList -import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase.Error.AccountCreation +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account @@ -14,19 +18,56 @@ import com.tangem.domain.models.wallet.UserWalletId import java.util.UUID /** + * Use case for adding a new crypto portfolio account + * + * @property crudRepository the repository used for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class AddCryptoPortfolioUseCase { +class AddCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** + * Adds a new crypto portfolio account to the repository + * + * @param userWalletId the unique identifier of the user wallet + * @param accountName the name of the new account + * @param icon the icon representing the new account + * @param derivationIndex the derivation index for the account + * + + */ suspend operator fun invoke( userWalletId: UserWalletId, accountName: AccountName, icon: CryptoPortfolioIcon, derivationIndex: Int, ): Either = either { - Account.CryptoPortfolio( + val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) + + val accountList = getAccountList(userWalletId = userWalletId).getOrElse { + createNewAccountList(userWalletId = userWalletId) + } + + val updatedAccounts = (accountList + newAccount) + .getOrElse { raise(Error.AccountListRequirementsNotMet(it)) } + + saveAccounts(updatedAccounts) + + newAccount + } + + private fun Raise.createAccount( + userWalletId: UserWalletId, + accountName: AccountName, + icon: CryptoPortfolioIcon, + derivationIndex: Int, + ): Account.CryptoPortfolio { + // TODO: [REDACTED_JIRA] + return Account.CryptoPortfolio( accountId = AccountId(userWalletId = userWalletId, value = UUID.randomUUID().toString()), - name = accountName.value, + accountName = accountName, accountIcon = icon, derivationIndex = derivationIndex, isArchived = false, @@ -36,20 +77,56 @@ class AddCryptoPortfolioUseCase { groupType = TokensGroupType.NONE, ), ) - .mapLeft(::AccountCreation) - .bind() - - // TODO: [REDACTED_JIRA] - // Save to local store - // Save to backend (tokens migration) – asynchronously + .getOrElse { raise(Error.AccountCreation(it)) } } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + private fun Raise.createNewAccountList(userWalletId: UserWalletId): AccountList { + val userWallet = catch( + block = { crudRepository.getUserWallet(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + + return AccountList.createEmpty(userWallet = userWallet) + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the add operation + */ sealed interface Error { + /** + * Error indicating that the account creation failed + * + * @property cause the underlying cause of the failure + */ data class AccountCreation(val cause: Account.CryptoPortfolio.Error) : Error - data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "Account list requirements not met: $cause" + } - data object DataOperationFailed : Error + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "Data operation failed: ${cause.message ?: "Unknown error"}" + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 04bb22f7b6..ca1a2d397b 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -4,11 +4,14 @@ import arrow.core.Either import arrow.core.left import com.google.common.truth.Truth import com.tangem.domain.account.utils.randomAccountId +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.wallet.UserWallet import io.mockk.clearMocks -import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -16,6 +19,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import kotlin.random.Random /** [REDACTED_AUTHOR] @@ -43,6 +47,44 @@ class AccountListTest { Truth.assertThat(actual).isEqualTo(expected) } + @Test + fun canAddMoreAccounts() { + // Arrange + val accountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(count = 2), + totalAccounts = 2, + ).getOrNull()!! + + val fullAccountList = AccountList( + userWallet = mockk(), + accounts = createAccounts(20), + totalAccounts = 20, + ).getOrNull()!! + + // Act & Assert + Truth.assertThat(accountList.canAddMoreAccounts).isTrue() + Truth.assertThat(fullAccountList.canAddMoreAccounts).isFalse() + } + + @Test + fun createEmpty() { + // Arrange + val userWallet = mockk(relaxed = true) + + // Act + val actual = AccountList.createEmpty(userWallet) + + // Assert + val expected = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ).getOrNull()!! + + Truth.assertThat(actual).isEqualTo(expected) + } + @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Create { @@ -129,6 +171,224 @@ class AccountListTest { val expected: Either, ) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Plus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: PlusTestModel) { + // Act + val actual = model.initial.plus(other = model.toAdd) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + // region Add new account + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + + val newAccount = createAccount(isMain = false) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, newAccount), + totalAccounts = 2, + ), + ) + }, + // endregion + // region Replace existing account + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + + val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!) + + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = newAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(newAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = createAccounts(20), + totalAccounts = 20, + ).getOrNull()!!, + toAdd = createAccount(isMain = false), + expected = AccountList.Error.ExceedsMaxAccountsCount.left(), + ), + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(createAccount(isMain = true)), + totalAccounts = 1, + ).getOrNull()!!, + toAdd = createAccount(isMain = true), + expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), + ), + PlusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf( + createAccount(isMain = true), + createAccount(isMain = false), + ), + totalAccounts = 2, + ).getOrNull()!!, + toAdd = createAccount(isMain = true), + expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), + ), + ) + } + + data class PlusTestModel( + val initial: AccountList, + val toAdd: Account, + val expected: Either, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Minus { + + private val userWallet = mockk() + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: MinusTestModel) { + // Act + val actual = model.initial.minus(model.toRemove) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + // region Remove existing account + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + + val secondaryAccount = createAccount( + accountId = AccountId(value = "2", userWalletId = mockk()), + isMain = false, + ) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = secondaryAccount, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + // region Remove unexisting account + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + val notInList = createAccount( + accountId = AccountId(value = "3", userWalletId = mockk()), + isMain = false, + ) + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = notInList, + expected = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ), + ) + }, + // endregion + // region EmptyAccountsList + run { + val mainAccount = createAccount(isMain = true) + + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount), + totalAccounts = 1, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.EmptyAccountsList.left(), + ) + }, + // endregion + // region MainAccountNotFound + run { + val mainAccount = createAccount( + accountId = AccountId(value = "1", userWalletId = mockk()), + isMain = true, + ) + val secondaryAccount = createAccount( + accountId = AccountId(value = "2", userWalletId = mockk()), + isMain = false, + ) + MinusTestModel( + initial = AccountList( + userWallet = userWallet, + accounts = setOf(mainAccount, secondaryAccount), + totalAccounts = 2, + ).getOrNull()!!, + toRemove = mainAccount, + expected = AccountList.Error.MainAccountNotFound.left(), + ) + }, + // endregion + ) + } + + data class MinusTestModel( + val initial: AccountList, + val toRemove: Account, + val expected: Either, + ) + private fun createAccounts(count: Int): Set { return buildSet { add(createAccount(isMain = true)) @@ -139,12 +399,22 @@ class AccountListTest { } private fun createAccount( - accountId: AccountId = AccountId(value = randomAccountId(5), userWalletId = mockk()), - isMain: Boolean = false, + accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), + accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + isMain: Boolean, ): Account.CryptoPortfolio { - return mockk { - every { this@mockk.accountId } returns accountId - every { this@mockk.isMainAccount } returns isMain - } + return Account.CryptoPortfolio( + accountId = accountId, + name = "Test Account", + accountIcon = accountIcon, + derivationIndex = if (isMain) 0 else Random.nextInt(1, 21), + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + .getOrNull()!! } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..9dc84f5b4d --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,317 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.util.UUID +import kotlin.random.Random + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AddCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = AddCryptoPortfolioUseCase(crudRepository) + + private val userWalletId = UserWalletId("011") + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should add new crypto portfolio account to existing list`() = runTest { + // Arrange + val existingAccount = createAccount( + name = "Main Account", + derivationIndex = 0, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId), + ) + + val accountList = AccountList( + userWallet = userWallet, + accounts = setOf(existingAccount), + totalAccounts = 1, + ).getOrNull()!! + + val fakeUUID = UUID.randomUUID() + + mockkStatic(UUID::class) + every { UUID.randomUUID() } returns fakeUUID + + val newAccount = createAccount( + accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + val updatedAccountList = (accountList + newAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } just Runs + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + + unmockkStatic(UUID::class) + } + + @Test + fun `invoke should create new account list if none exists`() = runTest { + // Arrange + val fakeUUID = UUID.randomUUID() + + mockkStatic(UUID::class) + every { UUID.randomUUID() } returns fakeUUID + + val newAccount = createAccount( + accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + val newAccountList = (AccountList.createEmpty(userWallet) + newAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet + coEvery { crudRepository.saveAccounts(newAccountList) } just Runs + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = newAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getUserWallet(userWalletId) + crudRepository.saveAccounts(newAccountList) + } + + unmockkStatic(UUID::class) + } + + @Test + fun `invoke should return error if account creation fails`() = runTest { + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = AccountName.Main, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = -1, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.AccountCreation( + cause = Account.CryptoPortfolio.Error.NegativeDerivationIndex, + ).left() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(inverse = true) { + crudRepository.getAccounts(any()) + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if account list requirements not met`() = runTest { + // Arrange + val accountList = AccountList( + userWallet = userWallet, + accounts = createAccounts(count = 20), + totalAccounts = 20, + ).getOrNull()!! + + val newAccount = createAccount( + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet( + cause = AccountList.Error.ExceedsMaxAccountsCount, + ).left() + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val newAccount = createAccount( + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + + coVerify(inverse = true) { + crudRepository.getUserWallet(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val existingAccount = createAccount( + name = "Main Account", + derivationIndex = 0, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId), + ) + + val accountList = AccountList( + userWallet = userWallet, + accounts = setOf(existingAccount), + totalAccounts = 1, + ).getOrNull()!! + + val fakeUUID = UUID.randomUUID() + + mockkStatic(UUID::class) + every { UUID.randomUUID() } returns fakeUUID + + val newAccount = createAccount( + accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = 1, + ) + + val updatedAccountList = (accountList + newAccount).getOrNull()!! + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + useCase( + userWalletId = userWalletId, + accountName = newAccount.name, + icon = newAccount.icon, + derivationIndex = newAccount.derivationIndex, + ) + + // Assert + // val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + // Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + + coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } + } + + private fun createAccounts(count: Int): Set { + return buildSet { + add(createAccount(derivationIndex = 0)) + repeat(count - 1) { + add(createAccount()) + } + } + } + + private fun createAccount( + accountId: AccountId? = null, + name: String = "Test Account", + icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex: Int = Random.nextInt(1, 21), + ): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = accountId ?: AccountId(value = UUID.randomUUID().toString(), userWalletId = userWalletId), + accountName = AccountName(name).getOrNull()!!, + accountIcon = icon, + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ).getOrNull()!! + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index 8d7b786cca..73789d0c65 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -176,6 +176,27 @@ sealed interface Account { ) } } + + /** + * Creates a main account for the given user wallet ID + * + * @param userWalletId the ID of the user wallet + */ + fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + // TODO: [REDACTED_JIRA] + return CryptoPortfolio( + accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + name = AccountName.Main, + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt index 532687ef6c..29fe653a8b 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountName.kt @@ -44,8 +44,13 @@ data class AccountName private constructor( companion object { + private const val MAIN_ACCOUNT_NAME = "Main Account" private const val MAX_LENGTH = 20 + /** Default name for the main account */ + val Main: AccountName + get() = AccountName(value = MAIN_ACCOUNT_NAME) + /** * Factory method to create an `AccountName` instance. * Validates the input string to ensure it is not blank and does not exceed the maximum length. diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt index d68bc46ab9..e2aee138be 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountNameTest.kt @@ -3,6 +3,7 @@ package com.tangem.domain.models.account import arrow.core.Either import arrow.core.left import com.google.common.truth.Truth +import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource @@ -13,6 +14,16 @@ import org.junit.jupiter.params.provider.MethodSource @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AccountNameTest { + @Test + fun main_returnsMainAccountName() { + // Act + val main = AccountName.Main.value + + // Assert + val expected = "Main Account" + Truth.assertThat(main).isEqualTo(expected) + } + @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: InvokeTestModel) { diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index d94c1a6bdf..dac8fdac45 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -3,6 +3,7 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account.CryptoPortfolio import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.currency.CryptoCurrency @@ -99,7 +100,7 @@ class AccountTest { val name = "" // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = mockk(), name = name, accountIcon = mockk(), @@ -120,7 +121,7 @@ class AccountTest { val derivationIndex = -1 // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = mockk(), name = "Test Account", accountIcon = mockk(), @@ -131,14 +132,14 @@ class AccountTest { .leftOrNull()!! // Assert - val expected = Account.CryptoPortfolio.Error.NegativeDerivationIndex + val expected = CryptoPortfolio.Error.NegativeDerivationIndex Truth.assertThat(actual).isEqualTo(expected) } @Test fun `invoke returns CryptoPortfolio`() { // Act - val actual = Account.CryptoPortfolio( + val actual = CryptoPortfolio( accountId = AccountId( value = "value", userWalletId = UserWalletId("011"), @@ -159,6 +160,32 @@ class AccountTest { val expected = createCryptoPortfolioStub() Truth.assertThat(actual).isEqualTo(expected) } + + @Test + fun createMainAccount() { + // Arrange + val userWalletId = UserWalletId("011") + + // Act + val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + // Assert + // TODO: [REDACTED_JIRA] + val expected = CryptoPortfolio( + accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + accountName = AccountName.Main, + accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + derivationIndex = 0, + isArchived = false, + cryptoCurrencyList = CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ).getOrNull() + + Truth.assertThat(actual).isEqualTo(expected) + } } private fun createCryptoPortfolioStub( @@ -166,8 +193,8 @@ class AccountTest { name: String = "Test Account", derivationIndex: Int = 0, currencies: Set = emptySet(), - ): Account.CryptoPortfolio { - return Account.CryptoPortfolio.invoke( + ): CryptoPortfolio { + return CryptoPortfolio.invoke( accountId = AccountId( value = "value", userWalletId = userWalletId, From 2336cb6aaa06cbcafbc415e18430dca29f8e7e73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 16:05:37 +0500 Subject: [PATCH 062/165] Updated on 2026-08-14 --- .../domain/walletconnect/WcAnalyticEvents.kt | 3 +- .../send/v2/api/params/FeeSelectorParams.kt | 3 ++ .../DefaultFeeSelectorBlockComponent.kt | 4 +- .../v2/feeselector/model/FeeSelectorModel.kt | 48 +++++++++++++++++++ .../v2/send/confirm/SendConfirmComponent.kt | 1 + .../confirm/SendWithSwapConfirmComponent.kt | 1 + .../components/common/WcNavigationUtils.kt | 2 + .../send/WcSendTransactionComponent.kt | 2 + 8 files changed, 61 insertions(+), 3 deletions(-) diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index d4bff77def..0d3291dca3 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -15,7 +15,7 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest sealed class WcAnalyticEvents( event: String, params: Map = mapOf(), -) : AnalyticsEvent(category = "Wallet Connect", event = event, params = params) { +) : AnalyticsEvent(category = WC_CATEGORY_NAME, event = event, params = params) { object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( @@ -228,5 +228,6 @@ sealed class WcAnalyticEvents( companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" + const val WC_CATEGORY_NAME = "Wallet Connect" } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 785002a836..80914b0ce9 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -16,6 +16,7 @@ sealed class FeeSelectorParams { abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus abstract val feeStateConfiguration: FeeStateConfiguration abstract val feeDisplaySource: FeeDisplaySource + abstract val analyticsCategoryName: String data class FeeSelectorBlockParams( override val state: FeeSelectorUM, @@ -24,6 +25,7 @@ sealed class FeeSelectorParams { override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val feeStateConfiguration: FeeStateConfiguration, override val feeDisplaySource: FeeDisplaySource, + override val analyticsCategoryName: String, ) : FeeSelectorParams() data class FeeSelectorDetailsParams( @@ -33,6 +35,7 @@ sealed class FeeSelectorParams { override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val feeStateConfiguration: FeeStateConfiguration, override val feeDisplaySource: FeeDisplaySource, + override val analyticsCategoryName: String, val callback: FeeSelectorModelCallback, ) : FeeSelectorParams() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 54be595d0a..7bfbe898e3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext @@ -50,6 +49,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( callback = model, feeStateConfiguration = params.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onDismiss = { model.feeSelectorBottomSheet.dismiss() @@ -79,7 +79,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( modifier = modifier .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { Modifier.clickable { - model.feeSelectorBottomSheet.activate(Unit) + model.showFeeSelector() } }, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index c655349774..2784044d44 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -3,8 +3,10 @@ package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.AmountType +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -12,13 +14,19 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadListener import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorCheckReloadTrigger import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadListener +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents +import com.tangem.features.send.v2.api.subcomponents.feeSelector.analytics.CommonSendFeeAnalyticEvents.GasPriceInserter import com.tangem.features.send.v2.feeselector.model.transformers.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update @@ -43,6 +51,7 @@ internal class FeeSelectorModel @Inject constructor( private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger, private val feeSelectorAlertFactory: FeeSelectorAlertFactory, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), FeeSelectorIntents, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -112,6 +121,11 @@ internal class FeeSelectorModel @Inject constructor( } override fun onFeeItemSelected(feeItem: FeeItem) { + if (feeItem is FeeItem.Custom) { + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = params.analyticsCategoryName), + ) + } uiState.update(FeeItemSelectedTransformer(feeItem)) } @@ -132,6 +146,27 @@ internal class FeeSelectorModel @Inject constructor( } override fun onDoneClick() { + val feeSelectorUM = uiState.value as? FeeSelectorUM.Content ?: return + analyticsEventHandler.send( + CommonSendFeeAnalyticEvents.SelectedFee( + categoryName = params.analyticsCategoryName, + feeType = feeSelectorUM.toAnalyticType(), + ), + ) + val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value + if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) { + analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName)) + } + if (feeSelectorUM.feeNonce is FeeNonce.Nonce) { + analyticsEventHandler.send( + NonceInserted( + categoryName = params.analyticsCategoryName, + token = params.feeCryptoCurrencyStatus.currency.symbol, + blockchain = params.feeCryptoCurrencyStatus.currency.network.name, + ), + ) + } + (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) } @@ -140,6 +175,19 @@ internal class FeeSelectorModel @Inject constructor( feeSelectorBottomSheet.dismiss() } + fun showFeeSelector() { + analyticsEventHandler.send( + CommonSendAnalyticEvents.FeeScreenOpened(categoryName = params.analyticsCategoryName), + ) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Fee, + ), + ) + feeSelectorBottomSheet.activate(Unit) + } + private fun subscribeOnFeeReloadTriggerUpdates() { feeSelectorReloadListener.reloadTriggerFlow .onEach { data -> diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 114939b743..a59c2345b7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -104,6 +104,7 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onResult = model::onFeeResult, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index d3cb5651c8..94f6d53022 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -89,6 +89,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( cryptoCurrencyStatus = model.primaryCurrencyStatus, feeStateConfiguration = FeeStateConfiguration.ExcludeLow, feeDisplaySource = FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, ), onResult = model::onFeeResult, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index d2a25d9cd4..292804e584 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.components.common import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 @@ -50,6 +51,7 @@ internal fun getWcCommonScreen( callback = model, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME, ), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 62f838c9b7..a974a6874f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.lifecycle.doOnResume import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams @@ -30,6 +31,7 @@ internal class WcSendTransactionComponent( feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, feeStateConfiguration = model.feeStateConfiguration, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME, ), onResult = model::updateFee, ) From beceddf1fb88b1be79742196545368fef1768587 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 16:06:04 +0500 Subject: [PATCH 063/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + .../core/analytics/models/AnalyticsParam.kt | 1 + features/manage-tokens/api/build.gradle.kts | 1 + .../component/ChooseManagedTokensComponent.kt | 1 + .../CommonManageTokensAnalyticEvents.kt | 33 +++++++++++++++++++ .../DefaultChooseManagedTokensComponent.kt | 2 ++ .../ChooseManageTokensBottomSheetConfig.kt | 1 + .../model/ChooseManagedTokensModel.kt | 21 ++++++++++++ .../DefaultSendEntryPointComponent.kt | 2 ++ .../SwapChooseTokenNetworkComponent.kt | 2 ++ .../v2/impl/amount/model/SwapAmountModel.kt | 1 + .../model/SwapChooseTokenNetworkModel.kt | 22 +++++++++++++ 13 files changed, 89 insertions(+) create mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 0f2952af12..7d7c0cc50d 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -442,6 +442,7 @@ internal class ChildFactory @Inject constructor( selectedCurrency = route.selectedCurrency, source = ChooseManagedTokensComponent.Source.valueOf(route.source.name), showSendViaSwapNotification = route.showSendViaSwapNotification, + analyticsCategoryName = route.analyticsCategoryName, ), componentFactory = chooseManagedTokensComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f36983b866..de65bd8448 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -134,6 +134,7 @@ sealed class AppRoute(val path: String) : Route { val selectedCurrency: CryptoCurrency?, val source: Source, val showSendViaSwapNotification: Boolean, + val analyticsCategoryName: String, ) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") { enum class Source { SendViaSwap, diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index b85be1b1aa..9257e90ce8 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -234,5 +234,6 @@ sealed class AnalyticsParam { const val RECEIVE_TOKEN = "Receive Token" const val SEND_BLOCKCHAIN = "Send Blockchain" const val RECEIVE_BLOCKCHAIN = "Receive Blockchain" + const val CHOSEN_TOKEN = "Token Chosen" } } \ No newline at end of file diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index c3a7b45a50..b1eb0e3830 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { /* Project - Core */ implementation(projects.core.ui) implementation(projects.core.decompose) + implementation(projects.core.analytics.models) /* Compose */ implementation(deps.compose.runtime) diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt index c5d28df18c..b3616e31a9 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ChooseManagedTokensComponent.kt @@ -14,6 +14,7 @@ interface ChooseManagedTokensComponent : ComposableContentComponent { val source: Source, val showSendViaSwapNotification: Boolean, val callback: ModelCallback? = null, + val analyticsCategoryName: String, ) enum class Source { diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt new file mode 100644 index 0000000000..eba0758c21 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/analytics/CommonManageTokensAnalyticEvents.kt @@ -0,0 +1,33 @@ +package com.tangem.features.managetokens.component.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.CHOSEN_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +sealed class CommonManageTokensAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + data class TokenSearchClicked( + val categoryName: String, + ) : CommonManageTokensAnalyticEvents(category = categoryName, event = "Token Search Clicked") + + /** Searched token chosen event */ + data class TokenSearched( + val categoryName: String, + val token: String?, + val blockchain: String?, + val isTokenChosen: Boolean, + ) : CommonManageTokensAnalyticEvents( + category = categoryName, + event = "Token Searched", + params = buildMap { + put(CHOSEN_TOKEN, if (isTokenChosen) "Yes" else "No") + token?.let { put(TOKEN_PARAM, token) } + blockchain?.let { put(BLOCKCHAIN, blockchain) } + }, + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index 99206f1591..1ff16dac71 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -59,9 +59,11 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( context = childByContext(componentContext), params = SwapChooseTokenNetworkComponent.Params( userWalletId = config.userWalletId, + analyticsCategoryName = params.analyticsCategoryName, initialCurrency = config.initialCurrency, selectedCurrency = config.selectedCurrency, token = config.token, + isSearchedToken = config.isSearchedToken, onDismiss = model.bottomSheetNavigation::dismiss, onResult = { swapCurrencies, cryptoCurrency -> componentScope.launch { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt index 77ee44682a..35d7732ad6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/entity/ChooseManageTokensBottomSheetConfig.kt @@ -14,5 +14,6 @@ internal sealed class ChooseManageTokensBottomSheetConfig { val initialCurrency: CryptoCurrency, val selectedCurrency: CryptoCurrency?, val token: ManagedCryptoCurrency.Token, + val isSearchedToken: Boolean, ) : ChooseManageTokensBottomSheetConfig() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 53b4245820..3c239162b1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -5,6 +5,7 @@ import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.ui.notifications.NotificationId import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -22,6 +23,7 @@ import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent.Source import com.tangem.features.managetokens.component.ManageTokensSource +import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents import com.tangem.features.managetokens.entity.item.CurrencyItemUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM @@ -40,12 +42,14 @@ import timber.log.Timber import javax.inject.Inject import kotlin.collections.isNotEmpty +@Suppress("LongParameterList") @ModelScoped internal class ChooseManagedTokensModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val uiMessageSender: UiMessageSender, private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { @@ -60,6 +64,7 @@ internal class ChooseManagedTokensModel @Inject constructor( initialCurrency = params.initialCurrency, selectedCurrency = params.selectedCurrency, token = token, + isSearchedToken = uiState.value.readContent.search.isActive, ), ) }, @@ -132,6 +137,17 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun observeSearchQueryChanges() { uiState .distinctUntilChanged { old, new -> + if (!new.readContent.search.isActive && old.readContent.search.isActive) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearched( + params.analyticsCategoryName, + token = null, + blockchain = null, + isTokenChosen = false, + ), + ) + } + // It's also used to skip search activation to avoid searching an empty query old.readContent.search.query == new.readContent.search.query && new.readContent.search.isActive } @@ -251,6 +267,11 @@ internal class ChooseManagedTokensModel @Inject constructor( } private fun toggleSearchBar(isActive: Boolean) { + if (isActive) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearchClicked(params.analyticsCategoryName), + ) + } uiState.update { state -> @StringRes val placeholderTextRes = if (isActive) { R.string.manage_tokens_search_placeholder diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt index 0985a678fb..67396045c9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/entrypoint/DefaultSendEntryPointComponent.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.api.SendEntryPointComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.entrypoint.model.SendEntryPointModel import com.tangem.features.swap.v2.api.SendWithSwapComponent import dagger.assisted.Assisted @@ -126,6 +127,7 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( selectedCurrency = null, showSendViaSwapNotification = showSendViaSwapNotification, callback = model, + analyticsCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY, ), ) } diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt index 15418e847b..8fa0dc468d 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkComponent.kt @@ -12,8 +12,10 @@ interface SwapChooseTokenNetworkComponent : ComposableBottomSheetComponent { data class Params( val userWalletId: UserWalletId, val initialCurrency: CryptoCurrency, + val analyticsCategoryName: String, val selectedCurrency: CryptoCurrency?, val token: ManagedCryptoCurrency.Token, + val isSearchedToken: Boolean, val onDismiss: () -> Unit, val onResult: (SwapCurrencies, CryptoCurrency) -> Unit, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 8ff3dc702c..5973ba9c7d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -270,6 +270,7 @@ internal class SwapAmountModel @Inject constructor( selectedCurrency = selectedCurrency.takeIf { isEditMode }, source = AppRoute.ChooseManagedTokens.Source.SendViaSwap, showSendViaSwapNotification = showSendViaSwapNotification, + analyticsCategoryName = params.analyticsCategoryName, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index 496e806384..a765bcee71 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -10,6 +11,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkContentUM import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.entity.SwapChooseTokenNetworkUM @@ -26,6 +29,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class SwapChooseTokenNetworkModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -34,6 +38,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private val createCryptoCurrencyUseCase: CreateCryptoCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val swapChooseTokenAlertFactory: SwapChooseTokenAlertFactory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require() @@ -103,6 +108,23 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private fun onSwapTokenClick(swapCurrencies: SwapCurrencies, cryptoCurrency: CryptoCurrency) { val prevSelectedCurrency = params.selectedCurrency?.network + analyticsEventHandler.send( + CommonSendAnalyticEvents.TokenChosen( + categoryName = params.analyticsCategoryName, + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + if (params.isSearchedToken) { + analyticsEventHandler.send( + CommonManageTokensAnalyticEvents.TokenSearched( + categoryName = params.analyticsCategoryName, + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + isTokenChosen = true, + ), + ) + } if (prevSelectedCurrency == null || cryptoCurrency.network == prevSelectedCurrency) { params.onResult(swapCurrencies, cryptoCurrency) } else { From b1e333b83b1d6630be7c5024cea44e346227674e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 12:42:35 +0500 Subject: [PATCH 064/165] Updated on 2026-08-14 --- .../data/swap/DefaultSwapRepositoryV2.kt | 40 +++++++++++++------ .../tangem/domain/swap/SwapRepositoryV2.kt | 8 ++-- .../swap/usecase/GetSwapPairsUseCase.kt | 3 ++ .../usecase/GetSwapSupportedPairsUseCase.kt | 3 ++ .../model/SwapChooseTokenNetworkModel.kt | 2 + 5 files changed, 39 insertions(+), 17 deletions(-) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index e2d1ac8603..82aa29d68a 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -66,6 +66,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List = withContext(coroutineDispatcher.default) { val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } @@ -73,6 +74,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, + swapTxType = swapTxType, ) val providers = expressRepository.getProviders( @@ -114,11 +116,13 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List = withContext(coroutineDispatcher.default) { val allPairs = getPairsInternal( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, + swapTxType = swapTxType, ) val providers = expressRepository.getProviders( @@ -308,24 +312,34 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet: UserWallet, initialCurrency: CryptoCurrency, cryptoCurrencyList: List, - ) = awaitAll( - // original pairs - async { + swapTxType: SwapTxType, + ) = when (swapTxType) { + SwapTxType.Swap -> awaitAll( + // original pairs + async { + invokePairRequest( + userWallet = userWallet, + from = arrayListOf(initialCurrency), + to = cryptoCurrencyList, + ) + }, + // reversed pairs + async { + invokePairRequest( + userWallet = userWallet, + from = cryptoCurrencyList, + to = arrayListOf(initialCurrency), + ) + }, + ).flatten() + SwapTxType.SendWithSwap -> { invokePairRequest( userWallet = userWallet, from = arrayListOf(initialCurrency), to = cryptoCurrencyList, ) - }, - // reversed pairs - async { - invokePairRequest( - userWallet = userWallet, - from = cryptoCurrencyList, - to = arrayListOf(initialCurrency), - ) - }, - ).flatten() + } + } private suspend fun invokePairRequest( userWallet: UserWallet, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index 6fa54f8771..b94e923f2f 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -6,10 +6,7 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapDataModel -import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.swap.models.SwapQuoteModel -import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.domain.swap.models.* import java.math.BigDecimal /** @@ -25,12 +22,14 @@ interface SwapRepositoryV2 { * @param initialCurrency currency being swapped (either to or from) * @param cryptoCurrencyStatusList list of currencies might be swapped * @param filterProviderTypes filters only specified provider types, if empty returns providers as is + * @param swapTxType swap tx type */ suspend fun getPairs( userWallet: UserWallet, initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List /** @@ -43,6 +42,7 @@ interface SwapRepositoryV2 { initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ): List /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt index 5dfe8cda5e..1284c416c3 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapPairsUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.swap.models.SwapTxType /** * Get list of swap pairs @@ -31,12 +32,14 @@ class GetSwapPairsUseCase( initialCurrency: CryptoCurrency, cryptoCurrencyStatusList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ) = Either.catch { val pairs = swapRepositoryV2.getPairs( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyStatusList = cryptoCurrencyStatusList, filterProviderTypes = filterProviderTypes, + swapTxType = swapTxType, ) val fromGroup = pairs.groupPairs( diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index 5f923070f5..b470cc5a4a 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.domain.swap.models.SwapCryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapCurrenciesGroup import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.swap.models.SwapTxType /** * Returns pais @@ -25,12 +26,14 @@ class GetSwapSupportedPairsUseCase( initialCurrency: CryptoCurrency, cryptoCurrencyList: List, filterProviderTypes: List, + swapTxType: SwapTxType, ) = Either.catch { val pairs = swapRepositoryV2.getSupportedPairs( userWallet = userWallet, initialCurrency = initialCurrency, cryptoCurrencyList = cryptoCurrencyList, filterProviderTypes = filterProviderTypes, + swapTxType = swapTxType, ) val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index a765bcee71..45659420c7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapTxType import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.managetokens.component.analytics.CommonManageTokensAnalyticEvents @@ -84,6 +85,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( initialCurrency = params.initialCurrency, cryptoCurrencyList = cryptoCurrencyList + params.initialCurrency, filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, + swapTxType = SwapTxType.SendWithSwap, ).getOrElse { Timber.e(it.toString()) uiState.update( From 2208f88e6b05902d98483b0c947df7bd11c27090 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 16:08:52 +0700 Subject: [PATCH 065/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 24 +++- .../com/tangem/common/routing/AppRoute.kt | 11 ++ features/account/api/build.gradle.kts | 3 + .../account/AccountCreateEditComponent.kt | 11 +- features/account/impl/build.gradle.kts | 1 + .../createedit/AccountCreateEditModel.kt | 105 ++++++++++++++-- .../DefaultAccountCreateEditComponent.kt | 2 + .../createedit/entity/AccountCreateEditUM.kt | 33 +++-- .../entity/AccountCreateEditUMBuilder.kt | 118 ++++++++++++++++++ .../createedit/ui/AccountCreateEditContent.kt | 30 ++++- 10 files changed, 302 insertions(+), 36 deletions(-) create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 7d7c0cc50d..ba64c1cfc6 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,13 +8,15 @@ import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent -import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent +import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -43,7 +45,6 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent -import com.tangem.features.home.api.HomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped @@ -85,6 +86,7 @@ internal class ChildFactory @Inject constructor( private val walletComponentFactory: WalletEntryComponent.Factory, private val sendComponentFactoryV2: SendComponent.Factory, private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory, + private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, @@ -497,6 +499,24 @@ internal class ChildFactory @Inject constructor( componentFactory = sendWithSwapComponentFactory, ) } + is AppRoute.CreateAccount -> { + createComponentChild( + context = context, + params = AccountCreateEditComponent.Params.Create( + userWalletId = route.userWalletId, + ), + componentFactory = accountCreateEditComponentFactory, + ) + } + is AppRoute.EditAccount -> { + createComponentChild( + context = context, + params = AccountCreateEditComponent.Params.Edit( + account = route.account, + ), + componentFactory = accountCreateEditComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index de65bd8448..3b71b7518e 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -9,6 +9,7 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId @@ -320,4 +321,14 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val currency: CryptoCurrency, ) : AppRoute(path = "/send_with_swap/${userWalletId.stringValue}/${currency.symbol}") + + @Serializable + data class CreateAccount( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/create_account/${userWalletId.stringValue}") + + @Serializable + data class EditAccount( + val account: Account, + ) : AppRoute(path = "/edit_account/${account.accountId.value}") } \ No newline at end of file diff --git a/features/account/api/build.gradle.kts b/features/account/api/build.gradle.kts index 349aee77c0..e3e35ac4cd 100644 --- a/features/account/api/build.gradle.kts +++ b/features/account/api/build.gradle.kts @@ -13,4 +13,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Project - Domain */ + implementation(projects.domain.models) } \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt index 1f6de5f0b2..b18a7db430 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -2,13 +2,20 @@ package com.tangem.features.account import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWalletId interface AccountCreateEditComponent : ComposableContentComponent { interface Factory : ComponentFactory sealed interface Params { - data object Create : Params - data object Edit : Params + data class Create( + val userWalletId: UserWalletId, + ) : Params + + data class Edit( + val account: Account, + ) : Params } } \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index 55714f3021..e68d20b144 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { /** Domain */ implementation(projects.domain.models) + implementation(projects.domain.account) /** Common */ implementation(projects.common.ui) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 872b7af23f..77818706a6 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -9,11 +9,22 @@ import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.createedit.entity.AccountCreateEditUM +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @ModelScoped @@ -22,24 +33,24 @@ internal class AccountCreateEditModel @Inject constructor( private val messageSender: UiMessageSender, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase, + private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, ) : Model() { private val params = paramsContainer.require() + private val umBuilder = AccountCreateEditUMBuilder(params) - val uiState: StateFlow - field = MutableStateFlow(TODO()) + val uiState: StateFlow get() = _uiState + private val _uiState = MutableStateFlow(value = getInitialState()) - init { - params - } - - fun unsaveChangeDialog() { - val firstAction = EventMessageAction( + private fun unsaveChangeDialog() { + val secondAction = EventMessageAction( title = resourceReference(R.string.account_unsaved_dialog_action_first), onClick = {}, ) - val secondAction = EventMessageAction( + val firstAction = EventMessageAction( title = resourceReference(R.string.account_unsaved_dialog_action_second), + warning = true, onClick = { router.pop() }, ) messageSender.send( @@ -51,4 +62,80 @@ internal class AccountCreateEditModel @Inject constructor( ), ) } + + private fun onConfirmClick() = modelScope.launch { + when (params) { + is AccountCreateEditComponent.Params.Create -> createNewCryptoPortfolio(params) + is AccountCreateEditComponent.Params.Edit -> editCryptoPortfolio(params) + } + } + + private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { + val state = uiState.value + val name = AccountName(state.account.name).getOrNull() ?: return + val icon = state.account.portfolioIcon + addCryptoPortfolioUseCase( + userWalletId = params.userWalletId, + accountName = name, + icon = icon, + derivationIndex = 0, // todo account + ) + } + + private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) { + val state = uiState.value + val name = AccountName(state.account.name).getOrNull() ?: return + val icon = state.account.portfolioIcon + val isNewName = name != params.account.name + val isNewIcon = icon != params.account.portfolioIcon + updateCryptoPortfolioUseCase( + icon = if (isNewIcon) icon else null, + name = if (isNewName) name else null, + accountId = params.account.accountId, + ) + } + + private fun onCloseClick() = unsaveChangeDialog() + + private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { + _uiState.value = uiState.value + .updateIconSelect(icon) + .validateNewState() + } + + private fun onColorSelect(color: CryptoPortfolioIcon.Color) { + _uiState.value = uiState.value + .updateColorSelect(color) + .validateNewState() + } + + private fun onNameChange(name: String) { + _uiState.value = uiState.value + .updateName(name) + .validateNewState() + } + + private fun AccountCreateEditUM.validateNewState(): AccountCreateEditUM { + val isValidName = AccountName(this.account.name).isRight() + val isAvailableForConfirm = when (params) { + is AccountCreateEditComponent.Params.Create -> isValidName + is AccountCreateEditComponent.Params.Edit -> { + val isNewName = this.account.name != params.account.name.value + val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon + isValidName && (isNewName || isNewIcon) + } + } + return this.updateButton(isButtonEnabled = isAvailableForConfirm) + } + + private fun getInitialState(): AccountCreateEditUM { + return AccountCreateEditUM( + title = umBuilder.toolbarTitle, + account = umBuilder.initAccountUM(::onNameChange), + colorsState = umBuilder.initColorsUM(::onColorSelect), + iconsState = umBuilder.initIconsUM(::onIconSelect), + buttonState = umBuilder.initButtonUM(::onConfirmClick), + onCloseClick = ::onCloseClick, + ) + } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt index 06725717ad..e920c6cbbe 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/DefaultAccountCreateEditComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.account.createedit +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -26,6 +27,7 @@ internal class DefaultAccountCreateEditComponent @AssistedInject constructor( modifier = modifier, state = state, ) + BackHandler(onBack = state.onCloseClick) } @AssistedFactory diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 4ef4877b97..2c7fd2d0f0 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -3,40 +3,39 @@ package com.tangem.features.account.createedit.entity import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf data class AccountCreateEditUM( - val title: TextReference = TextReference.EMPTY, - val account: Account = Account(), + val title: TextReference, + val account: Account, val colorsState: Colors, val iconsState: Icons, - val buttonState: Button = Button(), - val onCloseClick: () -> Unit = {}, + val buttonState: Button, + val onCloseClick: () -> Unit, ) { data class Account( - val name: TextReference = TextReference.EMPTY, - val portfolioIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - val derivationInfo: TextReference = TextReference.EMPTY, - val inputPlaceholder: TextReference = TextReference.EMPTY, - val onNameChange: (String) -> Unit = {}, + val name: String, + val portfolioIcon: CryptoPortfolioIcon, + val derivationInfo: TextReference, + val inputPlaceholder: TextReference, + val onNameChange: (String) -> Unit, ) data class Colors( val selected: CryptoPortfolioIcon.Color, - val list: ImmutableList = persistentListOf(), - val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit = {}, + val list: ImmutableList, + val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit, ) data class Icons( val selected: CryptoPortfolioIcon.Icon, - val list: ImmutableList = persistentListOf(), - val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit = {}, + val list: ImmutableList, + val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit, ) data class Button( - val isButtonEnabled: Boolean = false, - val onConfirmClick: () -> Unit = {}, - val text: TextReference = TextReference.EMPTY, + val isButtonEnabled: Boolean, + val onConfirmClick: () -> Unit, + val text: TextReference, ) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt new file mode 100644 index 0000000000..75f31f86ac --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -0,0 +1,118 @@ +package com.tangem.features.account.createedit.entity + +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.AccountCreateEditComponent +import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject + +internal class AccountCreateEditUMBuilder @Inject constructor( + val params: AccountCreateEditComponent.Params, +) { + + private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() + private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList() + private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount() + + val toolbarTitle: TextReference + get() = when (params) { + is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_title_create) + is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_title_edit) + } + + fun initAccountUM(onNameChange: (String) -> Unit): AccountCreateEditUM.Account { + return when (params) { + is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account( + name = "", + portfolioIcon = createIcon, + derivationInfo = TextReference.EMPTY, + inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), + onNameChange = onNameChange, + ) + is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( + name = params.account.name.value, + portfolioIcon = params.account.portfolioIcon, + derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ? + inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), + onNameChange = onNameChange, + ) + } + } + + fun initColorsUM(onColorSelect: (CryptoPortfolioIcon.Color) -> Unit): AccountCreateEditUM.Colors { + val selected: CryptoPortfolioIcon.Color = when (params) { + is AccountCreateEditComponent.Params.Create -> createIcon.color + is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.color + } + return AccountCreateEditUM.Colors( + selected = selected, + list = accountColors, + onColorSelect = onColorSelect, + ) + } + + fun initIconsUM(onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit): AccountCreateEditUM.Icons { + val selected: CryptoPortfolioIcon.Icon = when (params) { + is AccountCreateEditComponent.Params.Create -> createIcon.value + is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.value + } + return AccountCreateEditUM.Icons( + selected = selected, + list = accountIcons, + onIconSelect = onIconSelect, + ) + } + + fun initButtonUM(onConfirmClick: () -> Unit): AccountCreateEditUM.Button { + val text: TextReference = when (params) { + is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_create_button) + is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_edit_button) + } + return AccountCreateEditUM.Button( + isButtonEnabled = false, + onConfirmClick = onConfirmClick, + text = text, + ) + } + + internal companion object { + + val Account.portfolioIcon: CryptoPortfolioIcon + get() = when (this) { + is Account.CryptoPortfolio -> this.icon + } + + fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM { + val newIcon = CryptoPortfolioIcon.ofCustomAccount( + value = account.portfolioIcon.value, + color = color, + ) + return this.copy( + account = this.account.copy(portfolioIcon = newIcon), + colorsState = this.colorsState.copy(selected = color), + ) + } + + fun AccountCreateEditUM.updateIconSelect(icon: CryptoPortfolioIcon.Icon): AccountCreateEditUM { + val newIcon = CryptoPortfolioIcon.ofCustomAccount( + value = icon, + color = account.portfolioIcon.color, + ) + return this.copy( + account = this.account.copy(portfolioIcon = newIcon), + iconsState = this.iconsState.copy(selected = icon), + ) + } + + fun AccountCreateEditUM.updateName(name: String): AccountCreateEditUM { + return this.copy(account = this.account.copy(name = name)) + } + + fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM { + return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled)) + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index dd927df0f0..725964567d 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -33,6 +33,7 @@ import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.fields.AutoSizeTextField import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -66,7 +67,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi .weight(1f), ) { - AccountSummary(state.account, state.account.onNameChange) + AccountSummary(state.account) SpacerH24() AccountColor(state.colorsState) SpacerH24() @@ -91,7 +92,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi } @Composable -private fun AccountSummary(account: AccountCreateEditUM.Account, onNameChange: (String) -> Unit) { +private fun AccountSummary(account: AccountCreateEditUM.Account) { Column( modifier = Modifier .clip(RoundedCornerShape(16.dp)) @@ -115,9 +116,9 @@ private fun AccountSummary(account: AccountCreateEditUM.Account, onNameChange: ( centered = true, textStyle = TangemTheme.typography.head, placeholder = account.inputPlaceholder, - value = account.name.resolveReference(), + value = account.name, singleLine = true, - onValueChange = onNameChange, + onValueChange = account.onNameChange, ) SpacerH(20.dp) } @@ -133,9 +134,11 @@ private fun AccountIcon(account: AccountCreateEditUM.Account) { .background(account.portfolioIcon.color.getUiColor()), ) { val icon = account.portfolioIcon.value + val letter = account.name.firstOrNull() + ?: account.inputPlaceholder.resolveReference().first() when { icon == CryptoPortfolioIcon.Icon.Letter -> Text( - text = account.name.resolveReference().first().uppercase(), + text = letter.uppercase(), style = TangemTheme.typography.head, color = TangemTheme.colors.text.constantWhite, ) @@ -298,19 +301,27 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Date: Fri, 8 Aug 2025 15:26:34 +0400 Subject: [PATCH 066/165] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApiV2.kt | 17 - .../models/v2/UserTokensResponseV2.kt | 26 -- .../com/tangem/datasource/di/NetworkModule.kt | 300 +++--------------- .../datasource/di/utils/RetrofitApiBuilder.kt | 185 +++++++++++ .../tangem/datasource/utils/HttpClientExt.kt | 76 ----- 5 files changed, 221 insertions(+), 383 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt deleted file mode 100644 index 164cd09362..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApiV2.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.datasource.api.tangemTech - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.tangemTech.models.v2.UserTokensResponseV2 -import retrofit2.http.* - -interface TangemTechApiV2 { - - @GET("user-tokens/{wallet_id}") - suspend fun getUserTokens(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("user-tokens/{wallet_id}") - suspend fun saveUserTokens( - @Path("wallet_id") walletId: String, - @Body userTokens: UserTokensResponseV2, - ): ApiResponse -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt deleted file mode 100644 index e03b095d4e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/v2/UserTokensResponseV2.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.api.tangemTech.models.v2 - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse - -@JsonClass(generateAdapter = true) -data class UserTokensResponseV2( - @Json(name = "accounts") - val accounts: List, -) { - - @JsonClass(generateAdapter = true) - data class TokensAccount( - @Json(name = "id") - val id: Int, - @Json(name = "title") - val title: String, - @Json(name = "tokens") - val tokens: List? = null, - @Json(name = "tokensCount") - val tokensCount: Int? = null, - @Json(name = "archived") - val isArchived: Boolean, - ) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 2363c048e7..6c8dc4b626 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -1,8 +1,5 @@ package com.tangem.datasource.di -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.blockaid.BlockAidApi import com.tangem.datasource.api.common.config.ApiConfig @@ -12,44 +9,29 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager -import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.TangemTechApiV2 -import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.datasource.di.utils.RetrofitApiBuilder +import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.utils.* -import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.info.AppInfoProvider -import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import java.util.concurrent.TimeUnit import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) internal object NetworkModule { - private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L - private val excludedApiForLogging: Set = setOf( - ApiConfig.ID.StakeKit, - ) - @Provides @Singleton fun provideApiConfigManager( @@ -66,286 +48,76 @@ internal object NetworkModule { @Provides @Singleton - fun provideExpressApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): TangemExpressApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, + fun provideExpressApi(retrofitApiBuilder: RetrofitApiBuilder): TangemExpressApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.Express, + applyTimeoutAnnotations = false, ) } @Provides @Singleton - fun provideStakeKitApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - apiConfigsManager: ApiConfigsManager, - analyticsErrorHandler: AnalyticsErrorHandler, - appLogsStore: AppLogsStore, - ): StakeKitApi { - return createApi( - id = ApiConfig.ID.StakeKit, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, + fun provideStakeKitApi(retrofitApiBuilder: RetrofitApiBuilder): StakeKitApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.StakeKit, + applyTimeoutAnnotations = false, timeouts = Timeouts( callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, readTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, writeTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, ), - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, ) } @Provides @Singleton - fun provideOnrampApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): OnrampApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, + fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.Express, + applyTimeoutAnnotations = false, ) } @Provides @Singleton - fun provideTangemTechApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - ): TangemTechApi { - return createApi( - id = ApiConfig.ID.TangemTech, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { applyTimeoutAnnotations() }, - ) - } - - // TODO: It will be deleted in the future or refactored using ApiConfig - @Provides - @Singleton - fun provideTangemTechApiV2( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - appVersionProvider: AppVersionProvider, - appInfoProvider: AppInfoProvider, - ): TangemTechApiV2 { - return provideTangemTechApiInternal( - moshi = moshi, - context = context, - appVersionProvider = appVersionProvider, - baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, - analyticsErrorHandler = analyticsErrorHandler, - appInfoProvider = appInfoProvider, + fun provideTangemTechApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemTech, + applyTimeoutAnnotations = true, ) } @Provides @Singleton - fun provideTangemTechMarketsApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - ): TangemTechMarketsApi { - return createApi( - id = ApiConfig.ID.TangemTech, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .readTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .applyTimeoutAnnotations() - }, + fun provideTangemTechMarketsApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechMarketsApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemTech, + applyTimeoutAnnotations = false, + timeouts = Timeouts( + callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + ), + logsSaving = false, ) } @Provides @Singleton - fun provideTangemVisaApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): TangemPayApi { - return createApi( - id = ApiConfig.ID.TangemPay, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ).applyTimeoutAnnotations() - }, + fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.TangemPay, + applyTimeoutAnnotations = false, ) } - @Suppress("LongParameterList") - @Deprecated("use createApi instead") - private inline fun provideTangemTechApiInternal( - moshi: Moshi, - context: Context, - appVersionProvider: AppVersionProvider, - appInfoProvider: AppInfoProvider, - baseUrl: String, - analyticsErrorHandler: AnalyticsErrorHandler, - timeouts: Timeouts = Timeouts(), - requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider, appInfoProvider)), - ): T { - val client = OkHttpClient.Builder() - .applyTimeoutAnnotations() - .let { builder -> - var b = builder - if (timeouts.callTimeoutSeconds != null) { - b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.connectTimeoutSeconds != null) { - b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.readTimeoutSeconds != null) { - b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.writeTimeoutSeconds != null) { - b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) - } - b - } - .addHeaders( - *requestHeaders.toTypedArray(), - // TODO("refactor header init") get auth data after biometric auth to avoid race condition - // AuthenticationHeader(authProvider), - ) - .addLoggers(context) - .build() - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) - .baseUrl(baseUrl) - .client(client) - .build() - .create(T::class.java) - } - @Provides @Singleton - fun provideBlockAidApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): BlockAidApi { - return createApi( - id = ApiConfig.ID.BlockAid, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ).applyTimeoutAnnotations() - }, + fun provideBlockAidApi(retrofitApiBuilder: RetrofitApiBuilder): BlockAidApi { + return retrofitApiBuilder.build( + apiConfigId = ApiConfig.ID.BlockAid, + applyTimeoutAnnotations = false, ) } - - private inline fun createApi( - id: ApiConfig.ID, - moshi: Moshi, - context: Context, - apiConfigsManager: ApiConfigsManager, - analyticsErrorHandler: AnalyticsErrorHandler, - timeouts: Timeouts = Timeouts(), - clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this }, - ): T { - val environmentConfig = apiConfigsManager.getEnvironmentConfig(id) - - return Retrofit.Builder() - .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) - .baseUrl(environmentConfig.baseUrl) - .client( - OkHttpClient.Builder() - .applyApiConfig(id, apiConfigsManager) - .applyTimeoutAnnotations() - .let { builder -> - var b = builder - if (timeouts.callTimeoutSeconds != null) { - b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.connectTimeoutSeconds != null) { - b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.readTimeoutSeconds != null) { - b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) - } - if (timeouts.writeTimeoutSeconds != null) { - b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) - } - b - } - .addLoggers(context = context, id = id) - .clientBuilder() - .build(), - ) - .build() - .create(T::class.java) - } - - private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder { - if (id in excludedApiForLogging) return this - - return addLoggers(context) - } - - private data class Timeouts( - val callTimeoutSeconds: Long? = null, - val connectTimeoutSeconds: Long? = null, - val readTimeoutSeconds: Long? = null, - val writeTimeoutSeconds: Long? = null, - ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt new file mode 100644 index 0000000000..87de60fbd0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -0,0 +1,185 @@ +package com.tangem.datasource.di.utils + +import android.content.Context +import com.chuckerteam.chucker.api.ChuckerInterceptor +import com.squareup.moshi.Moshi +import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor +import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory +import com.tangem.datasource.api.utils.ConnectTimeout +import com.tangem.datasource.api.utils.ReadTimeout +import com.tangem.datasource.api.utils.WriteTimeout +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.datasource.utils.NetworkLogsSaveInterceptor +import com.tangem.datasource.utils.addHeaders +import dagger.hilt.android.qualifiers.ApplicationContext +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Invocation +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A builder class for creating Retrofit API instances + * + * @property apiConfigsManager manages API configurations for different environments + * @property moshi moshi + * @property analyticsErrorHandler handles analytics-related errors + * @property context application context + * @property appLogsStore application logs store + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class RetrofitApiBuilder @Inject constructor( + private val apiConfigsManager: ApiConfigsManager, + @NetworkMoshi private val moshi: Moshi, + private val analyticsErrorHandler: AnalyticsErrorHandler, + @ApplicationContext private val context: Context, + private val appLogsStore: AppLogsStore, +) { + + /** + * Builds a Retrofit API instance for the specified API configuration ID + * + * @param apiConfigId the ID of the API configuration to use + * @param applyTimeoutAnnotations whether to apply timeout annotations to the requests. See [ReadTimeout], etc. + * @param timeouts optional timeouts for the requests + * @param logsSaving whether to enable logs saving + * + * @return an instance [T] of the specified API interface + */ + inline fun build( + apiConfigId: ApiConfig.ID, + applyTimeoutAnnotations: Boolean, + timeouts: Timeouts? = null, + logsSaving: Boolean = true, + ): T { + val environmentConfig = apiConfigsManager.getEnvironmentConfig(apiConfigId) + + return Retrofit.Builder() + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) + .baseUrl(environmentConfig.baseUrl) + .client( + OkHttpClient.Builder() + .applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig) + .let { + if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it + } + .applyTimeouts(timeouts = timeouts) + .let { + if (logsSaving) it.applyLogsSaving() else it + } + .addLoggers(apiConfigId = apiConfigId, context = context) + .build(), + ) + .build() + .create(T::class.java) + } + + data class Timeouts( + val callTimeoutSeconds: Long? = null, + val connectTimeoutSeconds: Long? = null, + val readTimeoutSeconds: Long? = null, + val writeTimeoutSeconds: Long? = null, + ) + + private fun OkHttpClient.Builder.applyApiConfig( + apiConfigId: ApiConfig.ID, + environmentConfig: ApiEnvironmentConfig, + ): OkHttpClient.Builder { + return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { + addInterceptor( + interceptor = SwitchEnvironmentInterceptor(id = apiConfigId, apiConfigsManager = apiConfigsManager), + ) + } else { + val headers = environmentConfig.headers + + this.addHeaders(headers) + } + } + + private fun OkHttpClient.Builder.applyTimeouts(timeouts: Timeouts?): OkHttpClient.Builder { + if (timeouts == null) return this + + var b = this + + if (timeouts.callTimeoutSeconds != null) { + b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.connectTimeoutSeconds != null) { + b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.readTimeoutSeconds != null) { + b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.writeTimeoutSeconds != null) { + b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) + } + + return b + } + + /** + * Apply timeout annotations [Interceptor]. + * Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests. + */ + private fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder { + return addInterceptor( + Interceptor { chain -> + val request = chain.request() + val tag = request.tag(Invocation::class.java) + val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java) + val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) + val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) + + chain + .run { + connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .run { + readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .run { + writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this + } + .proceed(request) + }, + ) + } + + private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder { + return addInterceptor( + interceptor = NetworkLogsSaveInterceptor(appLogsStore), + ) + } + + private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder { + if (apiConfigId in excludedApiForLogging) return this + + return if (BuildConfig.LOG_ENABLED) { + addInterceptor(interceptor = ChuckerInterceptor(context)) + addInterceptor(interceptor = createNetworkLoggingInterceptor()) + } else { + this + } + } + + private companion object { + + val excludedApiForLogging: Set = setOf( + // ApiConfig.ID.StakeKit, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index ed7d4c62b3..eb040fb1e4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -1,21 +1,9 @@ package com.tangem.datasource.utils -import android.content.Context -import com.chuckerteam.chucker.api.ChuckerInterceptor -import com.tangem.datasource.BuildConfig -import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor -import com.tangem.datasource.api.common.config.ApiConfig -import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE -import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.datasource.api.utils.ConnectTimeout -import com.tangem.datasource.api.utils.ReadTimeout -import com.tangem.datasource.api.utils.WriteTimeout import com.tangem.utils.ProviderSuspend import kotlinx.coroutines.runBlocking import okhttp3.Interceptor import okhttp3.OkHttpClient -import retrofit2.Invocation /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder { @@ -24,30 +12,6 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade ) } -/** - * Apply timeout annotations [Interceptor]. - * Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests. - */ -internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder { - return addInterceptor( - Interceptor { chain -> - val request = chain.request() - val tag = request.tag(Invocation::class.java) - val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java) - val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java) - val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java) - - chain.run { - connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.run { - readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.run { - writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this - }.proceed(request) - }, - ) -} - /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders( requestHeaders: Map>, @@ -66,44 +30,4 @@ internal fun OkHttpClient.Builder.addHeaders( chain.proceed(request) }, ) -} - -/** - * Extension for logging each [OkHttpClient] request - * - * @param context context - */ -internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder { - return if (BuildConfig.LOG_ENABLED) { - context?.let { - addInterceptor(interceptor = ChuckerInterceptor(it)) - } - addInterceptor(interceptor = createNetworkLoggingInterceptor()) - } else { - this - } -} - -/** - * Apply api config - * - * @param id class of [ApiConfig] - * @param apiConfigsManager api configs manager - */ -internal fun OkHttpClient.Builder.applyApiConfig( - id: ApiConfig.ID, - apiConfigsManager: ApiConfigsManager, -): OkHttpClient.Builder { - return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { - addInterceptor( - interceptor = SwitchEnvironmentInterceptor( - id = id, - apiConfigsManager = apiConfigsManager, - ), - ) - } else { - val headers = apiConfigsManager.getEnvironmentConfig(id).headers - - this.addHeaders(headers) - } } \ No newline at end of file From 0a8e78877d3e93f911af552cba64ffde9040559d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 16:59:46 +0700 Subject: [PATCH 067/165] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../features/account/createedit/AccountCreateEditModel.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f40005c3c..c42bf7fcb6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -144,6 +144,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.data.account) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) implementation(projects.data.balanceHiding) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 77818706a6..e5b55f4c16 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -90,7 +90,7 @@ internal class AccountCreateEditModel @Inject constructor( val isNewIcon = icon != params.account.portfolioIcon updateCryptoPortfolioUseCase( icon = if (isNewIcon) icon else null, - name = if (isNewName) name else null, + accountName = if (isNewName) name else null, accountId = params.account.accountId, ) } From 06263e1193cc6f7d0d070133732d0eb089eabf87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 18:58:42 +0400 Subject: [PATCH 068/165] Updated on 2026-08-14 --- .../usecase/AddCryptoPortfolioUseCase.kt | 21 +-- .../domain/account/models/AccountListTest.kt | 163 +++++------------ .../usecase/AddCryptoPortfolioUseCaseTest.kt | 164 +++--------------- .../UpdateCryptoPortfolioUseCaseTest.kt | 129 +++++--------- .../tangem/domain/account/utils/AccountExt.kt | 43 ++++- .../tangem/domain/models/account/Account.kt | 52 +++--- .../tangem/domain/models/account/AccountId.kt | 26 ++- .../domain/models/account/DerivationIndex.kt | 59 +++++++ .../domain/models/account/AccountIdTest.kt | 44 +++++ .../domain/models/account/AccountTest.kt | 46 ++--- .../models/account/DerivationIndexTest.kt | 49 ++++++ 11 files changed, 360 insertions(+), 436 deletions(-) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt create mode 100644 domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 4e68e70e3a..8c9d43c18f 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -10,12 +10,8 @@ import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWalletId -import java.util.UUID /** * Use case for adding a new crypto portfolio account @@ -42,7 +38,7 @@ class AddCryptoPortfolioUseCase( userWalletId: UserWalletId, accountName: AccountName, icon: CryptoPortfolioIcon, - derivationIndex: Int, + derivationIndex: DerivationIndex, ): Either = either { val newAccount = createAccount(userWalletId, accountName, icon, derivationIndex) @@ -62,11 +58,10 @@ class AddCryptoPortfolioUseCase( userWalletId: UserWalletId, accountName: AccountName, icon: CryptoPortfolioIcon, - derivationIndex: Int, + derivationIndex: DerivationIndex, ): Account.CryptoPortfolio { - // TODO: [REDACTED_JIRA] return Account.CryptoPortfolio( - accountId = AccountId(userWalletId = userWalletId, value = UUID.randomUUID().toString()), + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = accountName, accountIcon = icon, derivationIndex = derivationIndex, @@ -77,7 +72,6 @@ class AddCryptoPortfolioUseCase( groupType = TokensGroupType.NONE, ), ) - .getOrElse { raise(Error.AccountCreation(it)) } } private suspend fun Raise.getAccountList(userWalletId: UserWalletId): Option { @@ -108,13 +102,6 @@ class AddCryptoPortfolioUseCase( */ sealed interface Error { - /** - * Error indicating that the account creation failed - * - * @property cause the underlying cause of the failure - */ - data class AccountCreation(val cause: Account.CryptoPortfolio.Error) : Error - /** * Error indicating that the account list requirements were not met. * diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index ca1a2d397b..69e846a06a 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -3,14 +3,13 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.left import com.google.common.truth.Truth -import com.tangem.domain.account.utils.randomAccountId -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import io.mockk.clearMocks import io.mockk.mockk import org.junit.jupiter.api.BeforeEach @@ -19,7 +18,6 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource -import kotlin.random.Random /** [REDACTED_AUTHOR] @@ -30,7 +28,7 @@ class AccountListTest { @Test fun mainAccount() { // Arrange - val mainAccount = createAccount(isMain = true) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) val accountList = AccountList( userWallet = mockk(), @@ -52,13 +50,13 @@ class AccountListTest { // Arrange val accountList = AccountList( userWallet = mockk(), - accounts = createAccounts(count = 2), + accounts = createAccounts(userWalletId = userWalletId, count = 2), totalAccounts = 2, ).getOrNull()!! val fullAccountList = AccountList( userWallet = mockk(), - accounts = createAccounts(20), + accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! @@ -116,50 +114,41 @@ class AccountListTest { expected = AccountList.Error.EmptyAccountsList.left(), ), CreateTestModel( - accounts = setOf(createAccount(isMain = false)), + accounts = setOf( + createAccount(userWalletId = userWalletId, derivationIndex = 1), + ), expected = AccountList.Error.MainAccountNotFound.left(), ), CreateTestModel( accounts = setOf( - createAccount(isMain = true), - createAccount(isMain = true), + Account.CryptoPortfolio.createMainAccount(userWalletId), + Account.CryptoPortfolio.createMainAccount(userWalletId).copy( + accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + ), ), expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), ), - createAccount(isMain = true).let { - CreateTestModel( - accounts = setOf(it), - expected = AccountList( - userWallet = userWallet, - accounts = setOf(it), - totalAccounts = 1, - ), - ) - }, - createAccounts(count = 20).let { + createAccounts(userWalletId = userWalletId, count = 1).let { CreateTestModel( accounts = it, - expected = AccountList( - userWallet = userWallet, - accounts = it, - totalAccounts = 20, - ), + expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1), + ) + }, + createAccounts(userWalletId = userWalletId, count = 20).let { + CreateTestModel( + accounts = it, + expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20), ) }, CreateTestModel( - accounts = createAccounts(21), + accounts = createAccounts(userWalletId = userWalletId, count = 21), expected = AccountList.Error.ExceedsMaxAccountsCount.left(), ), CreateTestModel( accounts = setOf( - createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ), - createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = false, - ), + createAccount(userWalletId = userWalletId, derivationIndex = 0), + createAccount(userWalletId = userWalletId, derivationIndex = 1), + createAccount(userWalletId = userWalletId, derivationIndex = 1), ), expected = AccountList.Error.DuplicateAccountIds.left(), ), @@ -190,12 +179,8 @@ class AccountListTest { private fun provideTestModels() = listOf( // region Add new account run { - val mainAccount = createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ) - - val newAccount = createAccount(isMain = false) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val newAccount = createAccount(userWalletId = userWalletId, derivationIndex = 1) PlusTestModel( initial = AccountList( @@ -214,11 +199,7 @@ class AccountListTest { // endregion // region Replace existing account run { - val mainAccount = createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ) - + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) val newAccount = mainAccount.copy(accountName = AccountName("New Name").getOrNull()!!) PlusTestModel( @@ -239,33 +220,12 @@ class AccountListTest { PlusTestModel( initial = AccountList( userWallet = userWallet, - accounts = createAccounts(20), + accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!!, - toAdd = createAccount(isMain = false), + toAdd = createAccount(userWalletId = userWalletId, derivationIndex = 21), expected = AccountList.Error.ExceedsMaxAccountsCount.left(), ), - PlusTestModel( - initial = AccountList( - userWallet = userWallet, - accounts = setOf(createAccount(isMain = true)), - totalAccounts = 1, - ).getOrNull()!!, - toAdd = createAccount(isMain = true), - expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), - ), - PlusTestModel( - initial = AccountList( - userWallet = userWallet, - accounts = setOf( - createAccount(isMain = true), - createAccount(isMain = false), - ), - totalAccounts = 2, - ).getOrNull()!!, - toAdd = createAccount(isMain = true), - expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), - ), ) } @@ -294,15 +254,8 @@ class AccountListTest { private fun provideTestModels() = listOf( // region Remove existing account run { - val mainAccount = createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ) - - val secondaryAccount = createAccount( - accountId = AccountId(value = "2", userWalletId = mockk()), - isMain = false, - ) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2) MinusTestModel( initial = AccountList( @@ -321,14 +274,9 @@ class AccountListTest { // endregion // region Remove unexisting account run { - val mainAccount = createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ) - val notInList = createAccount( - accountId = AccountId(value = "3", userWalletId = mockk()), - isMain = false, - ) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val notInList = createAccount(userWalletId = userWalletId, derivationIndex = 3) + MinusTestModel( initial = AccountList( userWallet = userWallet, @@ -346,7 +294,7 @@ class AccountListTest { // endregion // region EmptyAccountsList run { - val mainAccount = createAccount(isMain = true) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) MinusTestModel( initial = AccountList( @@ -361,14 +309,9 @@ class AccountListTest { // endregion // region MainAccountNotFound run { - val mainAccount = createAccount( - accountId = AccountId(value = "1", userWalletId = mockk()), - isMain = true, - ) - val secondaryAccount = createAccount( - accountId = AccountId(value = "2", userWalletId = mockk()), - isMain = false, - ) + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId) + val secondaryAccount = createAccount(userWalletId = userWalletId, derivationIndex = 2) + MinusTestModel( initial = AccountList( userWallet = userWallet, @@ -389,32 +332,8 @@ class AccountListTest { val expected: Either, ) - private fun createAccounts(count: Int): Set { - return buildSet { - add(createAccount(isMain = true)) - repeat(count - 1) { - add(createAccount(isMain = false)) - } - } - } + private companion object { - private fun createAccount( - accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), - accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - isMain: Boolean, - ): Account.CryptoPortfolio { - return Account.CryptoPortfolio( - accountId = accountId, - name = "Test Account", - accountIcon = accountIcon, - derivationIndex = if (isMain) 0 else Random.nextInt(1, 21), - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), - ) - .getOrNull()!! + val userWalletId = UserWalletId("011") } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index 9dc84f5b4d..1e7d026c11 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -7,11 +7,9 @@ import arrow.core.toOption import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -20,8 +18,6 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import java.util.UUID -import kotlin.random.Random @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AddCryptoPortfolioUseCaseTest { @@ -29,47 +25,21 @@ class AddCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = AddCryptoPortfolioUseCase(crudRepository) - private val userWalletId = UserWalletId("011") private val userWallet = mockk() @BeforeEach fun resetMocks() { clearMocks(crudRepository, userWallet) - - every { userWallet.walletId } returns userWalletId } @Test fun `invoke should add new crypto portfolio account to existing list`() = runTest { // Arrange - val existingAccount = createAccount( - name = "Main Account", - derivationIndex = 0, - icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId), - ) - - val accountList = AccountList( - userWallet = userWallet, - accounts = setOf(existingAccount), - totalAccounts = 1, - ).getOrNull()!! - - val fakeUUID = UUID.randomUUID() - - mockkStatic(UUID::class) - every { UUID.randomUUID() } returns fakeUUID - - val newAccount = createAccount( - accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), - name = "New Account", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = 1, - ) - + val newAccount = createNewAccount() + val accountList = AccountList.createEmpty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() - coEvery { crudRepository.saveAccounts(updatedAccountList) } just Runs // Act val actual = useCase( @@ -89,30 +59,16 @@ class AddCryptoPortfolioUseCaseTest { } coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } - - unmockkStatic(UUID::class) } @Test fun `invoke should create new account list if none exists`() = runTest { // Arrange - val fakeUUID = UUID.randomUUID() - - mockkStatic(UUID::class) - every { UUID.randomUUID() } returns fakeUUID - - val newAccount = createAccount( - accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), - name = "New Account", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = 1, - ) - + val newAccount = createNewAccount() val newAccountList = (AccountList.createEmpty(userWallet) + newAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns None coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet - coEvery { crudRepository.saveAccounts(newAccountList) } just Runs // Act val actual = useCase( @@ -131,32 +87,6 @@ class AddCryptoPortfolioUseCaseTest { crudRepository.getUserWallet(userWalletId) crudRepository.saveAccounts(newAccountList) } - - unmockkStatic(UUID::class) - } - - @Test - fun `invoke should return error if account creation fails`() = runTest { - // Act - val actual = useCase( - userWalletId = userWalletId, - accountName = AccountName.Main, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = -1, - ) - - // Assert - val expected = AddCryptoPortfolioUseCase.Error.AccountCreation( - cause = Account.CryptoPortfolio.Error.NegativeDerivationIndex, - ).left() - - Truth.assertThat(actual).isEqualTo(expected) - - coVerify(inverse = true) { - crudRepository.getAccounts(any()) - crudRepository.getUserWallet(any()) - crudRepository.saveAccounts(any()) - } } @Test @@ -164,15 +94,11 @@ class AddCryptoPortfolioUseCaseTest { // Arrange val accountList = AccountList( userWallet = userWallet, - accounts = createAccounts(count = 20), + accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! - val newAccount = createAccount( - name = "New Account", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = 1, - ) + val newAccount = createNewAccount(derivationIndex = 21) coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() @@ -202,12 +128,7 @@ class AddCryptoPortfolioUseCaseTest { @Test fun `invoke should return error if getAccounts throws exception`() = runTest { // Arrange - val newAccount = createAccount( - name = "New Account", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = 1, - ) - + val newAccount = createNewAccount() val exception = IllegalStateException("Test error") coEvery { crudRepository.getAccounts(userWalletId) } throws exception @@ -235,30 +156,8 @@ class AddCryptoPortfolioUseCaseTest { @Test fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange - val existingAccount = createAccount( - name = "Main Account", - derivationIndex = 0, - icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = userWalletId), - ) - - val accountList = AccountList( - userWallet = userWallet, - accounts = setOf(existingAccount), - totalAccounts = 1, - ).getOrNull()!! - - val fakeUUID = UUID.randomUUID() - - mockkStatic(UUID::class) - every { UUID.randomUUID() } returns fakeUUID - - val newAccount = createAccount( - accountId = AccountId(value = fakeUUID.toString(), userWalletId = userWalletId), - name = "New Account", - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = 1, - ) - + val newAccount = createNewAccount() + val accountList = AccountList.createEmpty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = IllegalStateException("Test error") @@ -267,7 +166,7 @@ class AddCryptoPortfolioUseCaseTest { coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception // Act - useCase( + val actual = useCase( userWalletId = userWalletId, accountName = newAccount.name, icon = newAccount.icon, @@ -275,8 +174,8 @@ class AddCryptoPortfolioUseCaseTest { ) // Assert - // val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() - // Truth.assertThat(actual).isEqualTo(expected) + val expected = AddCryptoPortfolioUseCase.Error.DataOperationFailed(cause = exception).left() + Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { crudRepository.getAccounts(userWalletId) @@ -286,32 +185,17 @@ class AddCryptoPortfolioUseCaseTest { coVerify(inverse = true) { crudRepository.getUserWallet(userWalletId) } } - private fun createAccounts(count: Int): Set { - return buildSet { - add(createAccount(derivationIndex = 0)) - repeat(count - 1) { - add(createAccount()) - } + private companion object { + + val userWalletId = UserWalletId("011") + + fun createNewAccount(derivationIndex: Int = 1): Account.CryptoPortfolio { + return createAccount( + userWalletId = userWalletId, + name = "New Account", + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + ) } } - - private fun createAccount( - accountId: AccountId? = null, - name: String = "Test Account", - icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex: Int = Random.nextInt(1, 21), - ): Account.CryptoPortfolio { - return Account.CryptoPortfolio( - accountId = accountId ?: AccountId(value = UUID.randomUUID().toString(), userWalletId = userWalletId), - accountName = AccountName(name).getOrNull()!!, - accountIcon = icon, - derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), - ).getOrNull()!! - } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index 6588216bc8..073c68e1d6 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -8,19 +8,17 @@ import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase.Error -import com.tangem.domain.account.utils.randomAccountId -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import kotlin.random.Random /** [REDACTED_AUTHOR] @@ -31,25 +29,24 @@ class UpdateCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) + private val userWallet = mockk() + @BeforeEach fun resetMocks() { - clearMocks(crudRepository) + clearMocks(crudRepository, userWallet) + + every { userWallet.walletId } returns userWalletId } @Test fun `invoke should update crypto portfolio account with new name`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount(accountId = accountId, isMain = true) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ) - .getOrNull()!! + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -57,11 +54,9 @@ class UpdateCryptoPortfolioUseCaseTest { val actual = useCase(accountId = accountId, accountName = newAccountName) // Assert - val updatedAccount = account.copy(accountName = newAccountName) val expected = updatedAccount.right() Truth.assertThat(actual).isEqualTo(expected) - val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) @@ -71,20 +66,15 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new icon`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount(accountId = accountId, isMain = true) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ) - .getOrNull()!! + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) + val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -92,11 +82,9 @@ class UpdateCryptoPortfolioUseCaseTest { val actual = useCase(accountId = accountId, icon = newAccountIcon) // Assert - val updatedAccount = account.copy(accountIcon = newAccountIcon) val expected = updatedAccount.right() Truth.assertThat(actual).isEqualTo(expected) - val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) @@ -106,21 +94,16 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new name and icon`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount(accountId = accountId, isMain = true) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ) - .getOrNull()!! + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -128,11 +111,9 @@ class UpdateCryptoPortfolioUseCaseTest { val actual = useCase(accountId = accountId, accountName = newAccountName, icon = newAccountIcon) // Assert - val updatedAccount = account.copy(accountName = newAccountName, accountIcon = newAccountIcon) val expected = updatedAccount.right() Truth.assertThat(actual).isEqualTo(expected) - val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coVerifyOrder { crudRepository.getAccounts(userWalletId = userWalletId) crudRepository.saveAccounts(accountList = updatedAccountList) @@ -142,15 +123,8 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if name and icon are null`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount(accountId = accountId, isMain = true) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ) - .getOrNull()!! + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -170,10 +144,11 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts throws exception`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! + val exception = IllegalStateException("Test exception") coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } throws exception @@ -192,8 +167,10 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts returns None`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) val accountList = None val newAccountName = AccountName("New name").getOrNull()!! @@ -214,18 +191,11 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts does not contain accountId`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount( - accountId = AccountId(value = "another-account-id", userWalletId = mockk()), - isMain = true, + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(1).getOrNull()!!, ) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ) - .getOrNull()!! val newAccountName = AccountName("New name").getOrNull()!! @@ -245,17 +215,11 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if saveAccounts throws exception`() = runTest { // Arrange - val accountId = AccountId(value = "test-account-id", userWalletId = mockk()) - val userWalletId = accountId.userWalletId - val account = createAccount(accountId = accountId, isMain = true) - val accountList = AccountList( - userWallet = mockk(), - accounts = setOf(account), - totalAccounts = 1, - ).getOrNull()!! + val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! - val updatedAccount = account.copy(accountName = newAccountName) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! val exception = IllegalStateException("Save failed") @@ -276,23 +240,8 @@ class UpdateCryptoPortfolioUseCaseTest { } } - private fun createAccount( - accountId: AccountId = AccountId(value = randomAccountId(length = 5), userWalletId = mockk()), - accountIcon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - isMain: Boolean, - ): Account.CryptoPortfolio { - return Account.CryptoPortfolio( - accountId = accountId, - name = "Test Account", - accountIcon = accountIcon, - derivationIndex = if (isMain) 0 else Random.nextInt(1, 21), - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), - ) - .getOrNull()!! + private companion object { + + val userWalletId = UserWalletId("011") } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt index 1186faab17..6246176a50 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -1,8 +1,41 @@ package com.tangem.domain.account.utils -fun randomAccountId(length: Int): String { - val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - return (1..length) - .map { chars.random() } - .joinToString("") +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.* +import com.tangem.domain.models.wallet.UserWalletId +import kotlin.random.Random + +fun createAccounts(userWalletId: UserWalletId, count: Int): Set { + return buildSet { + add(Account.CryptoPortfolio.createMainAccount(userWalletId)) + + repeat(count - 1) { + val account = createAccount(userWalletId = userWalletId, derivationIndex = it + 1) + + add(account) + } + } +} + +fun createAccount( + userWalletId: UserWalletId, + name: String = "Test Account", + icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex: Int = Random.nextInt(1, 21), +): Account.CryptoPortfolio { + val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!! + + return Account.CryptoPortfolio( + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), + accountName = AccountName(name).getOrNull()!!, + accountIcon = icon, + derivationIndex = derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index 73789d0c65..db4a5b164c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -2,10 +2,10 @@ package com.tangem.domain.models.account import arrow.core.Either import arrow.core.raise.either -import arrow.core.raise.ensure import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError +import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -43,14 +43,14 @@ sealed interface Account { override val accountId: AccountId, override val name: AccountName, val icon: CryptoPortfolioIcon, - val derivationIndex: Int, + val derivationIndex: DerivationIndex, val isArchived: Boolean, val cryptoCurrencyList: CryptoCurrencyList, ) : Account { /** Indicates if the account is the main account */ val isMainAccount: Boolean - get() = derivationIndex == 0 + get() = derivationIndex.isMain /** Number of tokens in the account */ val tokensCount: Int @@ -97,15 +97,11 @@ sealed interface Account { /** Error indicating that the account name is blank */ @Serializable - data class AccountNameError(val cause: AccountName.Error) : Error { - override fun toString(): String = cause.toString() - } + data class AccountNameError(val cause: AccountName.Error) : Error /** Error indicating that the derivation index is negative */ @Serializable - data object NegativeDerivationIndex : Error { - override fun toString(): String = "${this::class.simpleName}: Derivation index must be non-negative" - } + data class DerivationIndexError(val cause: DerivationIndex.Error) : Error } companion object { @@ -130,7 +126,8 @@ sealed interface Account { cryptoCurrencyList: CryptoCurrencyList, ): Either { return either { - val accountName = AccountName(name).mapLeft(::AccountNameError).bind() + val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind() + val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind() invoke( accountId = accountId, @@ -140,7 +137,6 @@ sealed interface Account { isArchived = isArchived, cryptoCurrencyList = cryptoCurrencyList, ) - .bind() } } @@ -159,22 +155,18 @@ sealed interface Account { accountId: AccountId, accountName: AccountName, accountIcon: CryptoPortfolioIcon, - derivationIndex: Int, + derivationIndex: DerivationIndex, isArchived: Boolean, cryptoCurrencyList: CryptoCurrencyList, - ): Either { - return either { - ensure(derivationIndex >= 0) { Error.NegativeDerivationIndex } - - CryptoPortfolio( - accountId = accountId, - name = accountName, - icon = accountIcon, - derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, - ) - } + ): CryptoPortfolio { + return CryptoPortfolio( + accountId = accountId, + name = accountName, + icon = accountIcon, + derivationIndex = derivationIndex, + isArchived = isArchived, + cryptoCurrencyList = cryptoCurrencyList, + ) } /** @@ -183,12 +175,16 @@ sealed interface Account { * @param userWalletId the ID of the user wallet */ fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { - // TODO: [REDACTED_JIRA] + val derivationIndex = DerivationIndex.Main + return CryptoPortfolio( - accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), name = AccountName.Main, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), - derivationIndex = 0, + derivationIndex = derivationIndex, isArchived = false, cryptoCurrencyList = CryptoCurrencyList( currencies = emptySet(), diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index 725f7143e9..a9cf874078 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -1,7 +1,10 @@ package com.tangem.domain.models.account +import com.tangem.common.extensions.toByteArray import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.extensions.toHexString import kotlinx.serialization.Serializable +import java.security.MessageDigest /** * Represents a unique identifier for an account @@ -10,7 +13,26 @@ import kotlinx.serialization.Serializable * @property userWalletId the identifier of the user wallet associated with the account */ @Serializable -data class AccountId( +data class AccountId private constructor( val value: String, val userWalletId: UserWalletId, -) \ No newline at end of file +) { + + companion object { + + private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") } + + /** + * Creates a unique account identifier for a crypto portfolio + * + * @param userWalletId the identifier of the user wallet + * @param derivationIndex the derivation index used to generate the identifier + */ + fun forCryptoPortfolio(userWalletId: UserWalletId, derivationIndex: DerivationIndex): AccountId { + val input = userWalletId.value + derivationIndex.value.toByteArray() + val value = sha256Digest.digest(input).toHexString() + + return AccountId(value = value, userWalletId = userWalletId) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt new file mode 100644 index 0000000000..5ffa4e2ecc --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/DerivationIndex.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import kotlinx.serialization.Serializable + +/** + * Represents a derivation index for accounts, ensuring validity and providing utility methods + * + * @property value the integer value of the derivation index + * +[REDACTED_AUTHOR] + */ +@Serializable +data class DerivationIndex private constructor( + val value: Int, +) { + + /** Checks if the derivation index corresponds to the main account */ + val isMain: Boolean + get() = value == MAIN_ACCOUNT_DERIVATION_INDEX + + /** + * Represents possible errors that can occur when creating a [DerivationIndex] + */ + @Serializable + sealed interface Error { + + /** Error indicating that the provided derivation index [derivationIndex] is invalid */ + @Serializable + data class NegativeDerivationIndex(val derivationIndex: Int) : Error { + override fun toString(): String { + return "${this::class.simpleName}: Derivation index cannot be negative: $derivationIndex" + } + } + } + + companion object { + + private const val MAIN_ACCOUNT_DERIVATION_INDEX = 0 + + /** Predefined instance of [DerivationIndex] for the main account */ + val Main: DerivationIndex = DerivationIndex(value = MAIN_ACCOUNT_DERIVATION_INDEX) + + /** + * Factory method to create a [DerivationIndex] instance + * + * @param value the integer value of the derivation index + * + * @return Either an error if the value is invalid, or a valid [DerivationIndex] instance + */ + operator fun invoke(value: Int): Either = either { + ensure(value >= 0) { Error.NegativeDerivationIndex(derivationIndex = value) } + + DerivationIndex(value) + } + } +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt new file mode 100644 index 0000000000..e988ec9fc1 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountIdTest.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.models.account + +import com.google.common.truth.Truth +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountIdTest { + + @ParameterizedTest + @MethodSource("provideTestModels") + fun forCryptoPortfolio(model: ForCryptoPortfolioModel) { + // Arrange + val userWalletId = UserWalletId("27163F47405CE73110837F24DF82607FF11C7AF9D78C93F409E4FEAFF3400C8F") + + // Act + val actual = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = model.derivationIndex) + + // Assert + Truth.assertThat(actual.value).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex.Main, + expected = "4E39B13EA11E3B35339664A10BEF48F4AF752A1CC2200F79D23CB0FB3396C63F", + ), + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex(1).getOrNull()!!, + expected = "7F22E71F8106783F0F2DAFCDE525E2F2A2281E864DDBE2FE668FA09329D563A2", + ), + ForCryptoPortfolioModel( + derivationIndex = DerivationIndex(42).getOrNull()!!, + expected = "555C1E17A302659446C97393453B7C2B3246AF4DA082C56C28FB6EDD1A6606A4", + ), + ) + + data class ForCryptoPortfolioModel( + val derivationIndex: DerivationIndex, + val expected: String, + ) +} \ No newline at end of file diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index dac8fdac45..eabb287672 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -115,38 +115,18 @@ class AccountTest { Truth.assertThat(actual).isEqualTo(expected) } - @Test - fun `invoke returns NegativeDerivationIndex`() { - // Arrange - val derivationIndex = -1 - - // Act - val actual = CryptoPortfolio( - accountId = mockk(), - name = "Test Account", - accountIcon = mockk(), - derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = mockk(), - ) - .leftOrNull()!! - - // Assert - val expected = CryptoPortfolio.Error.NegativeDerivationIndex - Truth.assertThat(actual).isEqualTo(expected) - } - @Test fun `invoke returns CryptoPortfolio`() { // Act + val derivationIndex = DerivationIndex.Main val actual = CryptoPortfolio( - accountId = AccountId( - value = "value", + accountId = AccountId.forCryptoPortfolio( userWalletId = UserWalletId("011"), + derivationIndex = derivationIndex, ), name = "Test Account", accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), - derivationIndex = 0, + derivationIndex = derivationIndex.value, isArchived = false, cryptoCurrencyList = CryptoCurrencyList( currencies = emptySet(), @@ -165,24 +145,27 @@ class AccountTest { fun createMainAccount() { // Arrange val userWalletId = UserWalletId("011") + val derivationIndex = DerivationIndex.Main // Act val actual = CryptoPortfolio.createMainAccount(userWalletId = userWalletId) // Assert - // TODO: [REDACTED_JIRA] val expected = CryptoPortfolio( - accountId = AccountId(userWalletId = userWalletId, value = "main_account"), + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), accountName = AccountName.Main, accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), - derivationIndex = 0, + derivationIndex = derivationIndex, isArchived = false, cryptoCurrencyList = CryptoCurrencyList( currencies = emptySet(), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), - ).getOrNull() + ) Truth.assertThat(actual).isEqualTo(expected) } @@ -194,11 +177,10 @@ class AccountTest { derivationIndex: Int = 0, currencies: Set = emptySet(), ): CryptoPortfolio { + val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!! + return CryptoPortfolio.invoke( - accountId = AccountId( - value = "value", - userWalletId = userWalletId, - ), + accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex), name = name, accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt new file mode 100644 index 0000000000..f7daa8a9b2 --- /dev/null +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/DerivationIndexTest.kt @@ -0,0 +1,49 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DerivationIndexTest { + + @Test + fun `isMain returns true only for main derivation index`() { + // Arrange + val main = DerivationIndex.Main + val notMain = DerivationIndex(1).getOrNull()!! + + // Act & Assert + Truth.assertThat(main.isMain).isTrue() + Truth.assertThat(notMain.isMain).isFalse() + } + + @ParameterizedTest + @MethodSource("provideTestModels") + fun invoke(model: InvokeTestModel) { + // Act + val actual = DerivationIndex(model.index) + + // Assert + Truth.assertThat(actual).isEqualTo(model.expected) + } + + private fun provideTestModels() = listOf( + InvokeTestModel(index = 0, expected = DerivationIndex.Main.right()), + InvokeTestModel(index = 5, expected = DerivationIndex(5).getOrNull()!!.right()), + InvokeTestModel(index = -1, expected = DerivationIndex.Error.NegativeDerivationIndex(-1).left()), + ) + + data class InvokeTestModel( + val index: Int, + val expected: Either, + ) +} \ No newline at end of file From abbfa57a2f4cc77203c4d09013070ce1cb17cc88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 21:28:44 +0400 Subject: [PATCH 069/165] Updated on 2026-08-14 --- .../tangem/domain/account/models/AccountList.kt | 10 +++++++++- .../account/usecase/AddCryptoPortfolioUseCase.kt | 7 ++++--- .../usecase/UpdateCryptoPortfolioUseCase.kt | 5 +++-- .../domain/account/models/AccountListTest.kt | 11 +++++++++-- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 8 +++++--- .../usecase/UpdateCryptoPortfolioUseCaseTest.kt | 14 +++++++------- .../com/tangem/domain/account/utils/AccountExt.kt | 6 +++++- .../account/createedit/AccountCreateEditModel.kt | 3 ++- 8 files changed, 44 insertions(+), 20 deletions(-) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 670a7b2bb6..319babf91b 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -108,6 +108,11 @@ data class AccountList private constructor( data object DuplicateAccountIds : Error { override fun toString(): String = "$tag: Account list contains duplicate account IDs" } + + @Serializable + data object DuplicateAccountNames : Error { + override fun toString(): String = "$tag: Account list contains duplicate account names" + } } companion object { @@ -144,6 +149,9 @@ data class AccountList private constructor( val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } + val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size + ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } + AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) } @@ -152,7 +160,7 @@ data class AccountList private constructor( * * @param userWallet the user wallet associated with the account list */ - fun createEmpty(userWallet: UserWallet): AccountList { + fun empty(userWallet: UserWallet): AccountList { return AccountList( userWallet = userWallet, accounts = setOf( diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 8c9d43c18f..75722f9824 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -46,8 +46,9 @@ class AddCryptoPortfolioUseCase( createNewAccountList(userWalletId = userWalletId) } - val updatedAccounts = (accountList + newAccount) - .getOrElse { raise(Error.AccountListRequirementsNotMet(it)) } + val updatedAccounts = (accountList + newAccount).getOrElse { + raise(Error.AccountListRequirementsNotMet(it)) + } saveAccounts(updatedAccounts) @@ -87,7 +88,7 @@ class AddCryptoPortfolioUseCase( catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - return AccountList.createEmpty(userWallet = userWallet) + return AccountList.empty(userWallet = userWallet) } private suspend fun Raise.saveAccounts(accountList: AccountList) { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index d73f3e9eaa..8c2208e552 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -50,8 +50,9 @@ class UpdateCryptoPortfolioUseCase( .setName(name = accountName) .setIcon(icon = icon) - val updatedAccounts = (accountList + updatedAccount) - .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) } + val updatedAccounts = (accountList + updatedAccount).getOrElse { + raise(Error.CriticalTechError.AccountListRequirementsNotMet(it)) + } saveAccounts(updatedAccounts) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 69e846a06a..d5323c0a85 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -66,12 +66,12 @@ class AccountListTest { } @Test - fun createEmpty() { + fun empty() { // Arrange val userWallet = mockk(relaxed = true) // Act - val actual = AccountList.createEmpty(userWallet) + val actual = AccountList.empty(userWallet) // Assert val expected = AccountList( @@ -152,6 +152,13 @@ class AccountListTest { ), expected = AccountList.Error.DuplicateAccountIds.left(), ), + CreateTestModel( + accounts = setOf( + createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 0), + createAccount(userWalletId = userWalletId, name = "Name", derivationIndex = 1), + ), + expected = AccountList.Error.DuplicateAccountNames.left(), + ), ) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index 1e7d026c11..cf47f9b807 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -30,13 +30,15 @@ class AddCryptoPortfolioUseCaseTest { @BeforeEach fun resetMocks() { clearMocks(crudRepository, userWallet) + + every { userWallet.walletId } returns userWalletId } @Test fun `invoke should add new crypto portfolio account to existing list`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.createEmpty(userWallet) + val accountList = AccountList.empty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() @@ -65,7 +67,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should create new account list if none exists`() = runTest { // Arrange val newAccount = createNewAccount() - val newAccountList = (AccountList.createEmpty(userWallet) + newAccount).getOrNull()!! + val newAccountList = (AccountList.empty(userWallet) + newAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns None coEvery { crudRepository.getUserWallet(userWalletId) } returns userWallet @@ -157,7 +159,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.createEmpty(userWallet) + val accountList = AccountList.empty(userWallet) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = IllegalStateException("Test error") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index 073c68e1d6..d6638b5c05 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -41,7 +41,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new name`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -66,7 +66,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new icon`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( @@ -94,7 +94,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new name and icon`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -123,7 +123,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if name and icon are null`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -144,7 +144,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -191,7 +191,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts does not contain accountId`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = AccountId.forCryptoPortfolio( userWalletId = userWalletId, derivationIndex = DerivationIndex(1).getOrNull()!!, @@ -215,7 +215,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if saveAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.createEmpty(userWallet = userWallet) + val accountList = AccountList.empty(userWallet = userWallet) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt index 6246176a50..597d6aa059 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -11,7 +11,11 @@ fun createAccounts(userWalletId: UserWalletId, count: Int): Set Date: Mon, 11 Aug 2025 16:34:37 +0500 Subject: [PATCH 070/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 35 ++++++++++++++++++- core/res/src/main/res/values-fr/strings.xml | 2 ++ core/res/src/main/res/values-ja/strings.xml | 31 +++++++++++++--- core/res/src/main/res/values-ru/strings.xml | 20 +++++++++-- .../src/main/res/values-uk-rUA/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 27 ++++++++++---- 7 files changed, 104 insertions(+), 15 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aa10a37bfa..673c284024 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1434,7 +1434,7 @@ Neue Verbindung Verbinde Deine Wallet mit einer anderen dApp Keine Sitzungen - Diese Domain wird von mehreren Sicherheitsanbietern als unsicher eingestuft. Verlasse diese umgehend, um Dein Vermögen zu schützen. + Es wurden potenzielle Risiken oder bösartiges Verhalten erkannt. Das Verbinden oder Signieren von Transaktionen kann zum Verlust von Geldern führen. Bekanntes Sicherheitsrisiko Anfrage von Art der Signatur diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index d075e7cd0c..40a09f5b5a 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1308,16 +1308,27 @@ Se requiere línea de confianza Dominio malicioso Dominio desconocido + Conectarse de todas formas + Error de tiempo de espera. Por favor, inténtalo de nuevo más tarde. + Error al establecer WalletConnect Este dominio no puede ser verificado. Compruebe cuidadosamente la solicitud de aprobación. Vuelva a su navegador y vuelva a conectarse a través de WalletConnect. + La sesión de Wallet Connect se desconectó Firmar de todos modos Código de error: %s. Si el problema persiste - no dude en ponerse en contacto con nuestro soporte. Si el problema persiste, no dudes en ponerte en contacto con nuestro soporte. + Hemos encontrado un error desconocido + Tangem Wallet actualmente no es compatible con %s. + dApp no compatible Código de error: 8 005. Si el problema persiste, no dudes en contactar con nuestro soporte. Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. + Redes no compatibles Tangem soporta una red requerida por %s + Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app + Tenemos algún tipo de problema + Todas las dApps desconectadas Permitir gastar Dirección Conectar @@ -1326,30 +1337,52 @@ Redes Ilimitado Billetera + Aplicación conectada Redes conectadas + Conectado a %1$s Ver el saldo y la actividad de su billetera Firmar transacciones sin previo aviso Solicitar aprobación para transacciones No podrá Me gustaría Solicitud de conexión + Conexiones Contenido Copiar datos Asignación personalizada + dApp desconectada + Desconectar todo + Texto sobre desconexión de todas las dApps + Desconectar todas las dApps Cambios estimados en la billetera La transacción no ha podido ser simulada. Por favor, proceda con precaución. + La estimación no es compatible con %s + Sugerido por %s + Recargue su saldo para cubrir la tarifa de red + Insuficiente %1$s Transacción maliciosa Añada la red %s a su perfil para esta billetera La billetera no tiene las redes requeridas - Este dominio está marcado como no seguro por varios proveedores de seguridad. Salga de inmediato para proteger sus activos + Nueva conexión + Conecta tu billetera a diferentes dApps + Sin sesiones + No se han detectado cambios en la cartera + Se han detectado riesgos potenciales o comportamiento malicioso. Conectarse o firmar transacciones puede resultar en la pérdida de fondos. Riesgo de seguridad conocido Solicitud de + Firmar de todos modos Tipo de firma + Se requiere al menos una red para la conexión dApp + Especificar las redes seleccionadas + Firmado correctamente A Solicitud de transacción Solicitud de transacción Cantidad ilimitada + Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único + URI ya utilizado Wallet connect + Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? Sí, reanudar diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a4140af715..bbf154df4e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1320,6 +1320,7 @@ Adresse Chargement Illimité + Réseaux connectés Connexions Contenu Copier les données @@ -1332,6 +1333,7 @@ Nouvelle connexion Connectez votre portefeuille à différentes dApps Aucune séance + Des risques potentiels ou un comportement malveillant ont été détectés. Se connecter ou signer des transactions peut entraîner une perte de fonds. Demande de Type de signature À diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a785951f19..a598944ae0 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -58,12 +58,16 @@ %1$s ネットワークのトークンは、ファームウェアの制限により、このカードまたはリングではサポートされていません。 カードまたはリングのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 + %1$sを使用すると、ウォレットのロックを迅速かつ安全に解除できます。また、取引の署名など、機密性の高い操作も承認できます。ハードウェアウォレットの場合は、署名にカードが必要です。 デフォルト手数料 デフォルト手数料を有効にすると、取引手数料が自動的に設定され、送金時に手数料ページを表示する必要がなくなります。必要に応じて、いつでもこのページに戻ることができます。 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする + %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + アクセスコードを要求する + このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 カードまたはリングとのやり取りには、アクセスコードの代わりに生体認証が要求されます。 ウォレットをアプリに保存する @@ -89,6 +93,7 @@ これらの%s個の単語をパスワードマネージャーなどの安全な場所に保存し、決して他の人と共有しないでください。 復元は不可能です リカバリーフレーズ + これらの単語は、絶対に誰にも共有しないでください。もし他人がこの単語を知れば、あなたの暗号資産をすべて盗むことができます。Tangemがこれらの単語を尋ねることはありません。以下の%s単語はウォレットの復元フレーズです。これらのフレーズを使用すると、デバイスを紛失した場合でもウォレットを復元できます。 これらの%s語を順番に書き留めて、安全かつプライベートに保管してください。 ウォレットと、リカバリーフレーズのセキュリティとバックアップの全責任は、Tangemではなくユーザーにあります。 リカバリーフレーズ @@ -182,6 +187,7 @@ 削除 + 無効にする 無効 切断 完了 @@ -236,6 +242,7 @@ 拒否 リロード 名前を変更 + 必須 保存 変更内容を保存 検索 @@ -332,6 +339,7 @@ 詳細 インターネット接続を確認するか、別のネットワークに切り替えてください。 利用規約 + 資金を受け取る 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する @@ -392,6 +400,7 @@ プロバイダー ベストレート FCA警告リスト + FCA警告リストに掲載されたプロバイダー 最大 %s まで使用可能 %s 以上で利用可能 このペアは利用できません @@ -443,6 +452,9 @@ セットアップを完了するには、ウォレットをバックアップし、アクセスコードを使用してアプリへのアクセスを保護します。 今すぐ実施 ウォレットのアクティベーションを完了する + セットアップを完了するには、アクセスコードを使用してアプリへのアクセスを保護します。 + そうした場合は、最初からやり直す必要があります。 + アクティベーションプロセスを終了してもよろしいですか? Googleドライブのバックアップに保存されている既存のウォレットを復元する Googleドライブのバックアップ バックアップへ移動 @@ -456,6 +468,7 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する + モバイルウォレット この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -1302,9 +1315,11 @@ Tangemウォレットを見る このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 アクセスコードの設定 / 変更 + アクセスコードの変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。 取引通知 + アクセスコードを設定する ウォレット設定 Tangem %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください @@ -1406,7 +1421,7 @@ WalletConnectを確立できませんでした このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 ブラウザに戻り、WalletConnect経由で再接続してください。 - WalletConnectセッションが接続解除されました + Wallet Connectセッションが接続解除されました とにかくサインする エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 @@ -1416,11 +1431,12 @@ エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 - サポートされていないネットワーク + 未対応のネットワーク Tangemは%sで必要なネットワークをサポートします 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています + すべてのdAppが接続解除されました 使用を許可する アドレス 接続する @@ -1431,6 +1447,7 @@ ウォレット 接続されたアプリ 接続されたネットワーク + %1$sへ接続済み ウォレットの残高とアクティビティを表示する 通知なしに取引に署名する 取引の承認をリクエストする @@ -1441,6 +1458,7 @@ 内容 データをコピー 使用可能量の設定 + dAppが接続解除されました すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する @@ -1448,20 +1466,23 @@ 取引をシミュレーションできませんでした。注意して続行してください。 %sでは見積もりはサポートされていません %sによる提案 + ネットワーク手数料をカバーするために残高を補充してください + %1$sが不足しています 悪意のある取引 このウォレットのポートフォリオに%sネットワークを追加します ウォレットに必要なネットワークはありません 新しい接続 - ウォレットを別のdAppに接続する + ウォレットをさまざまなdAppに接続する セッションなし ウォレットの変更は検出されませんでした - このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。 + 潜在的なリスクまたは悪意のある行為が検出されました。接続または取引への署名は資金の損失につながる可能性があります。 既知のセキュリティリスク リクエスト元 - とにかく送金 + とにかく署名する 署名タイプ dApp接続には、少なくとも1つのネットワークが必要です 選択したネットワークを指定する + 署名に成功しました 宛先 取引リクエスト 取引リクエスト diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 68950a67cc..10a53f974e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1304,15 +1304,18 @@ Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема + Все dapps отключены Разрешение на использование Адрес Подключение Загрузка Сеть Сети - Безлимитно + Без лимитно Кошелек + Подключенное приложение Подключенные сети + Подключено к %1$s Посмотреть баланс кошелька и его активность Подписать транзакцию без вашего участия Запрос разрешения на транзакцию @@ -1323,25 +1326,38 @@ Вложение Копировать данные Настраиваемый лимит + dApp отключен Отключить все Отключить все dApp Предварительные изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. + Оценка не поддерживается для %s + Предложено %s + Пополните ваш баланс, чтобы покрыть комиссию сети + Недостаточно %1$s Вредоносная транзакция Добавьте сеть %s в ваш портфель для выбранного кошелька В кошельке не добавлены необходимые сети Новое подключение Подключите свой кошелек к различным dApp Нет подключений - Этот домен помечен как небезопасный несколькими поставщиками систем безопасности. Немедленно покиньте его, чтобы защитить свои активы. + Изменения в кошельке не обнаружены. + Обнаружены потенциальные риски или вредоносное поведение. Подключение или подписание транзакций может привести к потере средств. Известный риск безопасности Запрос от + Подписать всё равно Тип подписи + Для подключения к dApp требуется как минимум одна выбранная сеть. + Укажите выбранные сети + Успешно подписано На Запрос транзакции Запрос транзакции Безлимитное количество + Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. + URI уже используется Подключение кошелька + Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? Да, возобновить diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index bc736e28ba..9c820fac97 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1286,10 +1286,12 @@ Обрана не вірна картка або кільце Адреса Підключення + Підключені мережі Переглянути баланс гаманця та активність Запит на підключення Вміст Копіювати дані + Виявлено потенційні ризики або шкідливу активність. Підключення чи підпис транзакцій можуть призвести до втрати коштів. Запит від Тип підпису До diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 37c1670198..62f1640aa6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,6 +12,10 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + Archive account + Archive + You are archiving this account, but you can always get it back. + Account Add account Save Account name @@ -58,12 +62,16 @@ Tokens in %1$s network are not supported by this card or ring due to firmware limitation. Are you having difficulty scanning your card or ring? This card is not designed to work with this app + Use %1$s to quickly and securely unlock your wallet and authorize all sensitive actions, such as signing transactions. For hardware wallets, you will still need a card to sign. Default Fee Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication + Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + Require Access Code + This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code Biometric authentication will be requested instead of the access code for interactions with your card or ring. Keep the wallet in the app @@ -188,6 +196,7 @@ days Delete + Disable Disabled Disconnect Done @@ -227,6 +236,7 @@ No No address Not Added + Not Now Now OK Open in Browser @@ -340,6 +350,7 @@ Details Check your internet connection or switch to a different network Terms of service + Receive assets Sending assets in other networks will result in permanent loss. %s network Send funds using only @@ -455,6 +466,7 @@ To complete setup, secure app access with Access Code. If you do, you\'ll need to start over. Are you sure you want to exit the activation process? + If you do, you\'ll need to start over. Recover an existing wallet stored in your Google Drive backup Google Drive Backup Go to backup @@ -977,12 +989,14 @@ Are you sure you want to change the token? After changing, previous data will be reset. Changing token Swap and send + Proceed with conversion? Previous data will be reset. + Confirm Convert Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive Will be sent a recipient Amount to receive Are you sure you want to cancel the conversion? After changing, previous data will be reset. - Confirm cancelation + Confirm cancellation Send with swap Transaction sent Prepare to scan card or ring you want to set up. @@ -1479,7 +1493,7 @@ Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. Please return to your browser and reconnect via WalletConnect. - WalletConnect session was disconnected + Wallet Connect session was disconnected Sign anyway Error code: %s. If the problem persists — feel free to contact our support. If the problem persists — feel free to contact our support. @@ -1489,7 +1503,7 @@ Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error Tangem does not currently support a required network by %s. - Unsuported networks + Unsupported networks Tangem support a required network by %s Verified domain Wrong card or ring selected in the App @@ -1530,17 +1544,18 @@ Add the %s network to your portfolio for this wallet The wallet has no required networks New connection - Connect your wallet to a different dApps + Connect your wallet to different dApps No sessions No wallet changes detected - This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets + Potential risks or malicious behavior have been detected. Connecting or signing transactions may lead to loss of funds. Known security risk Request from - Send anyway + Sign anyway Signature Type At least one network is required for dApp connection Specify selected networks Successfully signed + Wallet connect To Transaction request Transaction request From 06d083b283a539e8a2dc3715dcfbfa15262235d6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 16:36:04 +0500 Subject: [PATCH 071/165] Updated on 2026-08-14 --- .../common/ui/amountScreen/ui/AmountBlockV2.kt | 12 ++++++++---- .../DefaultChooseManagedTokensComponent.kt | 1 + .../amount/model/SendAmountAlertFactory.kt | 12 ++++++------ .../subcomponents/destination/ui/DestinationBlock.kt | 9 +++++---- .../v2/impl/amount/model/SwapAmountAlertFactory.kt | 11 ++++------- .../swap/v2/impl/amount/model/SwapAmountModel.kt | 2 +- .../model/SwapChooseTokenAlertFactory.kt | 12 ++++-------- .../success/ui/SendWithSwapSuccessContent.kt | 9 ++++++--- 8 files changed, 35 insertions(+), 33 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index b9fa864b84..ee080d579f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState @@ -24,6 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -43,7 +45,7 @@ fun AmountBlockV2( crypto( symbol = "", decimals = amount.cryptoAmount.decimals, - ) + ).uncapped() }.orEmpty() val fiatAmount = amount.fiatAmount.value.format { @@ -102,7 +104,7 @@ private fun AmountBlockV2( Row { Text( text = title.resolveReference(), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) SpacerWMax() @@ -121,18 +123,20 @@ private fun AmountBlockV2( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.padding(top = 8.dp), ) { - Text( + ResizableText( text = firstAmount, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, + maxLines = 1, ) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( + ResizableText( text = secondAmount, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + maxLines = 1, ) extraContent() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt index 1ff16dac71..7b18b4dc84 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/DefaultChooseManagedTokensComponent.kt @@ -72,6 +72,7 @@ internal class DefaultChooseManagedTokensComponent @AssistedInject constructor( cryptoCurrency = cryptoCurrency, shouldResetNavigation = params.selectedCurrency != null, ) + model.bottomSheetNavigation.dismiss() params.callback?.onResult() ?: router.pop() } }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt index dadd0ea67c..f4373fe4b9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountAlertFactory.kt @@ -2,9 +2,10 @@ package com.tangem.features.send.v2.subcomponents.amount.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.send.v2.impl.R import javax.inject.Inject @ModelScoped @@ -13,20 +14,19 @@ internal class SendAmountAlertFactory @Inject constructor( ) { fun showResetSendingAlert(onConfirm: () -> Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Confirm Convert"), - message = stringReference("Proceed with conversion? Previous data will be reset."), + title = resourceReference(R.string.send_with_swap_convert_token_alert_title), + message = resourceReference(R.string.send_with_swap_convert_token_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Confirm"), + title = resourceReference(R.string.common_confirm), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Not Now"), + title = resourceReference(R.string.common_not_now), onClick = onDismissRequest, ) }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index e1c1b5da9f..b3807ec5db 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -62,7 +63,7 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { Text( text = address.label.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, @@ -104,7 +105,7 @@ private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) { Text( text = memo.label.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Text( text = memo.value, @@ -121,7 +122,7 @@ private fun AddressWithMemoBlock( memo: DestinationTextFieldUM.RecipientMemo?, ) { Text( - text = stringResourceSafe(R.string.send_to_address), + text = stringResourceSafe(R.string.send_recipient), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -188,7 +189,7 @@ private class DestinationBlockPreviewProvider : PreviewParameterProvider Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Confirm cancellation"), - message = stringReference( - "Are you sure you want to cancel the conversion? After changing, previous data will be reset.", - ), + title = resourceReference(R.string.send_with_swap_remove_convert_alert_title), + message = resourceReference(R.string.send_with_swap_remove_convert_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Confirm"), + title = resourceReference(R.string.common_confirm), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Not Now"), + title = resourceReference(R.string.common_not_now), onClick = onDismissRequest, ) }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 5973ba9c7d..fb4748b307 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -661,7 +661,7 @@ internal class SwapAmountModel @Inject constructor( ).onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( - title = resourceReference(R.string.common_swap), + title = resourceReference(R.string.common_amount), subtitle = null, backIconRes = if (route.isEditMode) { R.drawable.ic_back_24 diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt index 70433b8957..c7605a26cc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenAlertFactory.kt @@ -3,7 +3,6 @@ package com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.features.swap.v2.impl.R @@ -26,22 +25,19 @@ internal class SwapChooseTokenAlertFactory @Inject constructor( } fun showChangeTokenAlert(onConfirm: () -> Unit, onDismiss: () -> Unit) { - // todo fix localization [REDACTED_TASK_KEY] uiMessageSender.send( DialogMessage( - title = stringReference("Changing token"), - message = stringReference( - "Are you sure you want to change the token? After changing, previous data will be reset.", - ), + title = resourceReference(R.string.send_with_swap_change_token_alert_title), + message = resourceReference(R.string.send_with_swap_change_token_alert_message), firstActionBuilder = { EventMessageAction( - title = stringReference("Change"), + title = resourceReference(R.string.common_change), onClick = onConfirm, ) }, secondActionBuilder = { EventMessageAction( - title = stringReference("Cancel"), + title = resourceReference(R.string.common_cancel), onClick = onDismiss, ) }, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index e730666446..2ad404315d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -125,9 +125,12 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { ), ), ) - .padding(top = 24.dp) - .padding(horizontal = 16.dp), - + .padding( + top = 24.dp, + bottom = 12.dp, + start = 16.dp, + end = 16.dp, + ), ) } } From e4294789b201cd6d7b8efaa41c3451cb93b06c55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 11:54:47 +0500 Subject: [PATCH 072/165] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 10 ++++++++++ .../ui/notifications/NotificationsFactory.kt | 14 +++++++++++++- gradle/tangem_dependencies.toml | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index aa0a20a5bb..ff58fd1c80 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -373,5 +373,15 @@ sealed class NotificationUM(val config: NotificationConfig) { formatArgs = wrappedList(rentInfo.exemptionAmount), ), ) + + data class RentExemptionDestination( + private val rentExemptionAmount: BigDecimal, + ) : Error( + title = TextReference.Res(R.string.send_notification_invalid_amount_title), + subtitle = TextReference.Res( + id = R.string.send_notification_invalid_amount_rent_destination, + formatArgs = wrappedList(rentExemptionAmount), + ), + ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 73f9baa570..c06f560367 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -354,7 +354,11 @@ object NotificationsFactory { onReduceClick = onReduceClick, ) is BlockchainSdkError.DestinationTagRequired -> addRequireDestinationTagErrorNotification() - null -> minAdaValue?.let { + is BlockchainSdkError.Solana.DestinationRentExemption -> addRentExemptionDestinationNotification( + rentExemptionAmount = validationError.rentAmount, + ) + null, + -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( tokenName = cryptoCurrency.name, @@ -439,6 +443,14 @@ object NotificationsFactory { add(NotificationUM.Solana.RentInfo(rentWarning)) } + fun MutableList.addRentExemptionDestinationNotification(rentExemptionAmount: BigDecimal) { + add( + NotificationUM.Solana.RentExemptionDestination( + rentExemptionAmount = rentExemptionAmount, + ), + ) + } + fun MutableList.addHighFeeWarningNotification( enteredAmountValue: BigDecimal, cryptoCurrencyStatus: CryptoCurrencyStatus, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 78d485789e..e880536eea 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1129" +tangemBlockchainSdk = "develop-1133" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From b93507349dcb2b6012bce09dad4a5c952790e277 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 21:34:39 +0300 Subject: [PATCH 073/165] Updated on 2026-08-14 --- .../destination/entity/DestinationTextFieldUM.kt | 9 +++++++++ .../v2/subcomponents/destination/ui/DestinationBlock.kt | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt index 55cc94b302..b0bbb0fc76 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/entity/DestinationTextFieldUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.api.subcomponents.destination.entity import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import com.tangem.utils.toBriefAddressFormat @Immutable sealed class DestinationTextFieldUM { @@ -27,6 +28,10 @@ sealed class DestinationTextFieldUM { val actualAddress: String get() = blockchainAddress ?: value + + // if value is human-readable address, this field contains the actual brief blockchain address + val briefBlockchainAddress: String? + get() = blockchainAddress?.toBriefAddressFormat(BRIEF_ADDRESS_EDGE_LENGTH, BRIEF_ADDRESS_EDGE_LENGTH) } data class RecipientMemo( @@ -40,4 +45,8 @@ sealed class DestinationTextFieldUM { val isEnabled: Boolean, val isValuePasted: Boolean, ) : DestinationTextFieldUM() + + private companion object { + const val BRIEF_ADDRESS_EDGE_LENGTH = 13 + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index e1c1b5da9f..5cdfd469ac 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -82,7 +82,7 @@ private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) - val blockchainAddress = address.blockchainAddress + val blockchainAddress = address.briefBlockchainAddress if (!blockchainAddress.isNullOrBlank()) { Text( text = blockchainAddress, @@ -136,7 +136,7 @@ private fun AddressWithMemoBlock( style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) - val blockchainAddress = address.blockchainAddress + val blockchainAddress = address.briefBlockchainAddress if (!blockchainAddress.isNullOrBlank()) { Text( text = blockchainAddress, From 0d2c403a6a401a90f762aa5eb5d5f00a19808b8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 19:36:13 +0500 Subject: [PATCH 074/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 - common/ui/build.gradle.kts | 1 - core/ui/build.gradle.kts | 1 - .../com/tangem/core/ui/res/TangemTheme.kt | 56 ++++++++++++++----- .../impl/build.gradle.kts | 1 - features/disclaimer/impl/build.gradle.kts | 1 - features/hot-wallet/impl/build.gradle.kts | 1 - features/manage-tokens/impl/build.gradle.kts | 1 - features/markets/impl/build.gradle.kts | 1 - features/nft/impl/build.gradle.kts | 1 - .../features/nft/details/ui/NFTDetailsLogo.kt | 2 +- features/onboarding-v2/impl/build.gradle.kts | 1 - features/onramp/impl/build.gradle.kts | 1 - features/staking/impl/build.gradle.kts | 1 - features/swap/impl/build.gradle.kts | 1 - features/tester/impl/build.gradle.kts | 1 - features/tokendetails/impl/build.gradle.kts | 1 - features/wallet/impl/build.gradle.kts | 1 - gradle/dependencies.toml | 2 - 19 files changed, 44 insertions(+), 33 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c42bf7fcb6..31cb895042 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -260,13 +260,11 @@ dependencies { /** Compose libraries */ implementation(deps.compose.constraintLayout) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.navigation.hilt) implementation(deps.compose.shimmer) implementation(deps.compose.ui) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index f6f607f75a..2cb8f794be 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { /** Compose */ implementation(deps.compose.material3) - implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 7c76425a6e..3f46d7909d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -42,7 +42,6 @@ dependencies { /** Compose */ implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.paging) implementation(deps.compose.ui.tooling) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index b6410efd58..bc5f89a78c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -4,9 +4,9 @@ import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors -import androidx.compose.material.Colors -import androidx.compose.material.MaterialTheme -import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color @@ -90,7 +90,7 @@ fun TangemTheme( val rootBackgroundColor = rememberedColors.background.secondary MaterialTheme( - colors = materialThemeColors(colors = themeColors, isDark = isDark), + colorScheme = tangemColorScheme(colors = themeColors), ) { CompositionLocalProvider( LocalTangemColors provides rememberedColors, @@ -143,21 +143,51 @@ object TangemTheme { @Stable @Composable -private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { - return Colors( +private fun tangemColorScheme(colors: TangemColors): ColorScheme { + return ColorScheme( primary = colors.background.primary, - primaryVariant = colors.background.secondary, - secondary = colors.button.primary, - secondaryVariant = colors.text.accent, - background = colors.background.primary, - surface = colors.background.secondary, - error = colors.text.warning, onPrimary = colors.text.primary1, + primaryContainer = colors.background.secondary, + onPrimaryContainer = colors.background.action, + inversePrimary = colors.background.action, + + secondary = colors.button.primary, onSecondary = colors.text.primary1, + secondaryContainer = colors.background.secondary, + onSecondaryContainer = colors.text.primary1, + + tertiary = colors.background.tertiary, + onTertiary = colors.text.tertiary, + tertiaryContainer = colors.background.tertiary, + onTertiaryContainer = colors.text.tertiary, + + background = colors.background.primary, onBackground = colors.text.primary1, + + surface = colors.background.secondary, + surfaceVariant = colors.background.tertiary, onSurface = colors.text.primary1, + onSurfaceVariant = colors.text.secondary, + surfaceTint = colors.background.tertiary, + inverseSurface = colors.button.disabled, + inverseOnSurface = colors.button.primary, + surfaceBright = colors.background.secondary, + surfaceDim = colors.background.tertiary, + surfaceContainer = colors.background.tertiary, + surfaceContainerHigh = colors.background.tertiary, + surfaceContainerHighest = colors.background.tertiary, + surfaceContainerLow = colors.background.tertiary, + surfaceContainerLowest = colors.background.tertiary, + + error = colors.text.warning, + errorContainer = colors.background.tertiary, + onErrorContainer = colors.text.primary2, onError = colors.text.primary2, - isLight = !isDark, + + outline = colors.stroke.primary, + outlineVariant = colors.stroke.secondary, + + scrim = colors.stroke.transparency, ) } diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 828a0ccfac..477a6af3ea 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index 3454262136..b34ba43590 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.accompanist.permission) implementation(deps.compose.accompanist.webView) implementation(deps.compose.material3) - implementation(deps.compose.material) /** Core modules */ implementation(projects.core.ui) diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index c740fecd23..ccc57778cd 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index c6a2a28283..0506848d40 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.foundation) - implementation(deps.compose.material) // For button colors implementation(deps.compose.material3) implementation(deps.compose.shimmer) diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 6029226493..df2755faef 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 83bba9f0b5..0e4b9f5c67 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt index 465271494a..2a156d878d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index eac8270d6f..9653f895cb 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -62,7 +62,6 @@ dependencies { implementation(deps.lifecycle.runtime.ktx) /** Compose libraries */ - implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index efcd01bc52..8280707bae 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -63,7 +63,6 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) - implementation(deps.compose.material) /** Other */ implementation(deps.decompose.ext.compose) diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 67246e25f1..995576c43d 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.material3) - implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 5d0745ca74..ed1a7f3f50 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -61,7 +61,6 @@ dependencies { /** Compose */ implementation(deps.arrow.core) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index bd1dd6bf0f..6be7aca95c 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4e929dfdb3..4bcd8ce768 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -21,7 +21,6 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.coil) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 77dd37708d..e6d1f16109 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) implementation(deps.compose.foundation) - implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.paging) implementation(deps.compose.reorderable) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3aee2f0af5..f9ef6078d0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -30,7 +30,6 @@ androidxWorkManager = "2.9.0" # region Compose compose-runtime = "1.7.4" compose-foundation = "1.7.4" -compose-material = "1.7.4" compose-material3 = "1.3.1" compose-constraint = "1.0.1" compose-navigation = "2.7.7" @@ -169,7 +168,6 @@ compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } -compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } compose-constraintLayout = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "compose-constraint" } compose-shimmer = { module = "com.valentinilk.shimmer:compose-shimmer", version.ref = "compose-shimmer" } From 1c5fa97fdfcd65d5fcf98e056816ec26ee37a62f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 11:43:40 +0300 Subject: [PATCH 075/165] Updated on 2026-08-14 --- .../com/tangem/common/utils/WireMockUtils.kt | 23 ++++++++++ .../kotlin/com/tangem/tests/BuyTokenTest.kt | 45 +++++++++++++------ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt index eb4bc13f9e..fe4c30d9a6 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt @@ -8,12 +8,18 @@ import java.io.IOException /** * Method uses to set WireMock scenario state + * @param scenarioName Name of the scenario to modify + * @param state The target state to set (must be one of the scenario's possibleStates) + * @param baseUrl WireMock base URL + * @return true if state was set successfully, false otherwise */ fun setWireMockScenarioState( scenarioName: String, state: String, baseUrl: String = "[REDACTED_ENV_URL]" ): Boolean { + Timber.i("=== WireMock Scenario Set ===") + Timber.i("Setting scenario '$scenarioName' to state: $state") val client = OkHttpClient() val json = """{"state": "$state"}""" val mediaType = "application/json".toMediaType() @@ -93,4 +99,21 @@ fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean { Timber.e(e, "Exception during reset") false } +} + +/** + * Method to reset a specific WireMock scenario to its initial state + * @param scenarioName Name of the scenario to reset + * @param initialState The target state to reset the scenario to (must be one of the scenario's possibleStates) + * @param baseUrl WireMock base URL + * @return true if reset was successful, false otherwise + */ +fun resetWireMockScenarioState( + scenarioName: String, + initialState: String = "Started", + baseUrl: String = "[REDACTED_ENV_URL]" +): Boolean { + Timber.i("=== WireMock Scenario Reset ===") + Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState") + return setWireMockScenarioState(scenarioName, initialState, baseUrl) } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index fb942f0550..83adfe0bb1 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -3,7 +3,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.TOTAL_BALANCE import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.utils.resetWireMockScenarios +import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.OpenMainScreenScenario import com.tangem.screens.* @@ -19,18 +19,20 @@ class BuyTokenTest : BaseTestCase() { @DisplayName("Onramp: error in providers loading") @Test fun errorInProvidersLoadingTest() { + val scenarioName = "payment_methods" + val tokenTitle = "Bitcoin" + val balance = TOTAL_BALANCE + setupHooks( additionalAfterSection = { - resetWireMockScenarios() + resetWireMockScenarioState(scenarioName) } ).run { - val tokenTitle = "Bitcoin" - val balance = TOTAL_BALANCE - - resetWireMockScenarios() - - step("Setup WireMock scenario for 'Error' state") { - setWireMockScenarioState("payment_methods", "Error") + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } + step("Setup WireMock scenario '$scenarioName' for 'Error' state") { + setWireMockScenarioState(scenarioName, "Error") } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) @@ -74,8 +76,11 @@ class BuyTokenTest : BaseTestCase() { val australianDollar = "AUD" val fiatAmount = "1" val tokenAmount = "POL 488.24938338" + val scenarioName = "payment_methods" - resetWireMockScenarios() + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) @@ -162,8 +167,11 @@ class BuyTokenTest : BaseTestCase() { val euro = "EUR" val fiatAmount = "1" val tokenAmount = "POL 488.24938338" + val scenarioName = "payment_methods" - resetWireMockScenarios() + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) @@ -234,8 +242,11 @@ class BuyTokenTest : BaseTestCase() { val balance = TOTAL_BALANCE val country = "Albania" val unavailableCountry = "Lebanon" + val scenarioName = "payment_methods" - resetWireMockScenarios() + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) @@ -320,8 +331,11 @@ class BuyTokenTest : BaseTestCase() { val tokenAmount = "POL 488.24938338" val bestRate = "Best rate" val rate = "-0.00%" + val scenarioName = "payment_methods" - resetWireMockScenarios() + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) @@ -422,8 +436,11 @@ class BuyTokenTest : BaseTestCase() { val invoiceRevolutPay = "Invoice Revolut Pay" val sepa = "Sepa" val fiatAmount = "1" + val scenarioName = "payment_methods" - resetWireMockScenarios() + step("Reset WireMock scenario '$scenarioName'") { + resetWireMockScenarioState(scenarioName) + } step("Open 'Main Screen'") { scenario(OpenMainScreenScenario(composeTestRule)) From e5408a2ac713dcc22912cf90d49286051e0e5fc6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 17:22:39 +0300 Subject: [PATCH 076/165] Updated on 2026-08-14 --- .../tangem/common/constants/TestConstants.kt | 2 + .../screens/SelectNetworkFeePageObject.kt | 42 +++ .../tangem/screens/SwapStoriesPageObject.kt | 20 ++ .../com/tangem/screens/SwapTokenPageObject.kt | 80 ++++++ .../tangem/screens/TokenDetailsPageObject.kt | 42 +++ .../kotlin/com/tangem/tests/BuyTokenTest.kt | 5 +- .../kotlin/com/tangem/tests/SwapTokenTest.kt | 240 ++++++++++++++++++ .../ui/swapStoriesScreen/SwapStoriesScreen.kt | 5 +- .../buttons/HorizontalActionChips.kt | 6 +- .../ui/components/buttons/actions/Actions.kt | 4 +- .../ui/components/inputrow/InputRowDefault.kt | 5 +- .../ui/components/rows/SelectorRowItem.kt | 5 +- .../ui/components/stories/StoriesContainer.kt | 5 +- .../SelectNetworkFeeBottomSheetTestTags.kt | 6 + .../core/ui/test/SwapStoriesScreenTestTags.kt | 6 + .../core/ui/test/SwapTokenScreenTestTags.kt | 15 ++ .../ui/test/TokenDetailsScreenTestTags.kt | 3 + .../feature/swap/ui/ChooseFeeBottomSheet.kt | 5 +- .../tangem/feature/swap/ui/ProviderItem.kt | 6 +- .../feature/swap/ui/SwapScreenContent.kt | 5 +- .../tangem/feature/swap/ui/TransactionCard.kt | 25 +- .../ui/components/TokenInfoBlock.kt | 3 + .../plugin/configuration/model/BuildType.kt | 2 +- 23 files changed, 517 insertions(+), 20 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index 29774a64fd..00cfc3afe3 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -2,4 +2,6 @@ package com.tangem.common.constants object TestConstants { const val TOTAL_BALANCE = "$3,299.18" + + const val WAIT_UNTIL_TIMEOUT = 20_000L } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt new file mode 100644 index 0000000000..35a55722df --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SelectNetworkFeePageObject.kt @@ -0,0 +1,42 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.wallet.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_fee_selector_title)) + useUnmergedTree = true + } + + val marketSelectorItem: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM) + hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market))) + useUnmergedTree = true + } + + val fastSelectorItem: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM) + hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast))) + useUnmergedTree = true + } + + val readMoreTextBlock: KNode = child { + hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt new file mode 100644 index 0000000000..e48a981112 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt @@ -0,0 +1,20 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.SwapStoriesScreenTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val closeButton: KNode = child { + hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt new file mode 100644 index 0000000000..3ba4c711b5 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -0,0 +1,80 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.NotificationTestTags +import com.tangem.core.ui.test.SwapTokenScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag + +class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_swap)) + useUnmergedTree = true + } + + val closeButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + + val textInput: KNode = child { + hasParent(withTestTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD)) + useUnmergedTree = true + } + + val networkFeeBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK) + useUnmergedTree = true + } + + val receiveAmountShimmer: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) + } + + val swapTokensOnscreenButton: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BUTTON) + } + + val receiveAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD) + useUnmergedTree = true + } + + val providersBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK) + useUnmergedTree = true + } + + val errorNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + useUnmergedTree = true + } + + val errorNotificationText: KNode = child { + hasTestTag(NotificationTestTags.TEXT) + useUnmergedTree = true + } + + val refreshButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.warning_button_refresh)) + } + + val swapButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_swap)) + } + +} + +internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index 2f8eae1c51..e6bb7ddeb4 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -1,11 +1,18 @@ package com.tangem.screens +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.TokenDetailsScreenTestTags +import com.tangem.core.ui.utils.LazyListItemPositionSemantics +import com.tangem.features.tokendetails.impl.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode +import io.github.kakaocup.kakao.common.utilities.getResourceString class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -13,6 +20,41 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide val screenContainer: KNode = child { hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) } + + val title: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE) + } + + private val horizontalActionChips = KLazyListNode( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS) }, + itemTypeBuilder = { itemType(::LazyListItemNode) }, + positionMatcher = { position -> + SemanticsMatcher.expectValue( + LazyListItemPositionSemantics, + position + ) + } + ) + + @OptIn(ExperimentalTestApi::class) + val swapButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_swap)) + } + + @OptIn(ExperimentalTestApi::class) + val sellButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_sell)) + } + + @OptIn(ExperimentalTestApi::class) + val buyButton: LazyListItemNode = horizontalActionChips.childWith { + hasTestTag(TokenDetailsScreenTestTags.ACTION_BUTTON) + hasText(getResourceString(R.string.common_buy)) + } + } internal fun BaseTestCase.onTokenDetailsScreen(function: TokenDetailsPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 83adfe0bb1..62365124a9 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -2,6 +2,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState @@ -372,14 +373,14 @@ class BuyTokenTest : BaseTestCase() { } step("Assert unavailable provider name is displayed") { onSelectProviderBottomSheet { - flakySafely(timeoutMs = 20_000) { + flakySafely(WAIT_UNTIL_TIMEOUT) { unavailableProviderItem.assertIsDisplayed() } } } step("Assert available provider name is displayed") { onSelectProviderBottomSheet { - flakySafely(timeoutMs = 20_000) { + flakySafely(WAIT_UNTIL_TIMEOUT) { availableProviderItem.assertIsDisplayed() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt new file mode 100644 index 0000000000..638218b875 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt @@ -0,0 +1,240 @@ +package com.tangem.tests + +import androidx.compose.ui.test.hasText +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapTokenTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("3546") + @DisplayName("Swap: network fee") + @Test + fun networkFeeTest() { + val inputAmount = "100" + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'Close' button is displayed") { + onSwapTokenScreen { closeButton.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert receive amount is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + receiveAmount.assertIsDisplayed() + } + } + } + step("Input swap amount = '$inputAmount'") { + composeTestRule.waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Assert 'Providers' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + providersBlock.assertIsDisplayed() + } + } + } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.assertIsDisplayed() + } + } + } + step("Assert receive amount is not equal to '0'") { + onSwapTokenScreen { receiveAmount.assert(!hasText("0")) } + } + } + } + + @AllureId("3549") + @DisplayName("Swap: network error test") + @Test + fun networkErrorSwapTest() { + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert error notification title is displayed") { + onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() } + } + step("Assert error notification text is displayed") { + onSwapTokenScreen { errorNotificationText.assertIsDisplayed() } + } + step("Assert 'Refresh' button is displayed") { + onSwapTokenScreen { refreshButton.assertIsDisplayed() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("3546") + @DisplayName("Swap: change network fee") + @Test + fun changeNetworkFeeTest() { + val inputAmount = "100" + setupHooks().run { + val tokenTitle = "Polygon" + val balance = TOTAL_BALANCE + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Click on token with name: '$tokenTitle'") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert receive amount is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + receiveAmount.assertIsDisplayed() + } + } + } + step("Input swap amount = '$inputAmount'") { + composeTestRule.waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Click on 'Network fee' block") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.clickWithAssertion() + } + } + } + step("Assert 'Select fee' bottom sheet title is displayed") { + onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() } + } + step("Assert 'Market' item is displayed") { + onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() } + } + step("Assert 'Fast' item is displayed") { + onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() } + } + step("Assert 'Read more' text block is displayed") { + onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() } + } + step("Click on 'Fast' item") { + onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() } + } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + networkFeeBlock.assertIsDisplayed() + } + } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt index 94b925980a..dede83288b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -29,6 +30,7 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapStoriesScreenTestTags import kotlinx.collections.immutable.persistentListOf private val SubtitleColor = Color(0xFFB0B0B0) @@ -46,7 +48,8 @@ fun SwapStoriesScreen(config: SwapStoriesUM) { Box( modifier = Modifier .fillMaxSize() - .background(TangemColorPalette.Black), + .background(TangemColorPalette.Black) + .testTag(SwapStoriesScreenTestTags.SCREEN_CONTAINER), ) { SubcomposeAsyncImage( modifier = Modifier.fillMaxSize(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt index c7f0ba8246..8f5326a498 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -21,6 +22,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -32,7 +34,9 @@ fun HorizontalActionChips( contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0), ) { LazyRow( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), verticalAlignment = Alignment.CenterVertically, contentPadding = contentPadding, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 77f984d3c8..a7fa9386f9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -33,6 +34,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags /** * Rounded action button @@ -98,7 +100,7 @@ fun ActionButton( ), ) }, - modifier = modifier, + modifier = modifier.testTag(TokenDetailsScreenTestTags.ACTION_BUTTON), color = color, containerColor = containerColor, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index cf5d0d6153..2d0c1b4fb1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -26,6 +27,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -64,7 +66,8 @@ fun InputRowDefault( ) { Column( modifier = Modifier - .weight(1f), + .weight(1f) + .testTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK), ) { title?.let { Text( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 86009a8f7f..1499aaecaf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -25,6 +26,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.utils.StringsSigns @Composable @@ -68,7 +70,8 @@ fun SelectorRowItem( Row( modifier = Modifier .fillMaxWidth() - .padding(paddingValues), + .padding(paddingValues) + .testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM), verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt index 629f9d10cc..bc292ff0d0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/StoriesContainer.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +29,7 @@ import com.tangem.core.ui.components.stories.model.StoryConfig import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapStoriesScreenTestTags import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -101,7 +103,8 @@ inline fun StoriesContainer( interactionSource = remember { MutableInteractionSource() }, indication = LocalIndication.current, onClick = { config.onClose(watchedCounter) }, - ), + ) + .testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt new file mode 100644 index 0000000000..3d2af738bd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SelectNetworkFeeBottomSheetTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SelectNetworkFeeBottomSheetTestTags { + const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT" + const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt new file mode 100644 index 0000000000..40a0ffe621 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SwapStoriesScreenTestTags { + const val SCREEN_CONTAINER = "SWAP_STORIES_SCREEN_CONTAINER" + const val CLOSE_BUTTON = "SWAP_STORIES_SCREEN_CLOSE_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt new file mode 100644 index 0000000000..d8be9a8e43 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.test + +object SwapTokenScreenTestTags { + const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK" + const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE" + const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" + const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" + const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" + const val NETWORK_FEE_BLOCK = "SWAP_TOKEN_SCREEN_NETWORK_FEE_BLOCK" + const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" + const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" + const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" + const val TOKEN_NAME = "SWAP_TOKEN_SCREEN_TOKEN_NAME" + const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index 54bc9893ca..f337c0bbc9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -2,4 +2,7 @@ package com.tangem.core.ui.test object TokenDetailsScreenTestTags { const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE" + const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON" + const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS" } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 836783751f..1932d52634 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -19,6 +20,7 @@ import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.FeeItemState @@ -89,7 +91,8 @@ private fun FooterBlock(readMore: TextReference, readMoreUrl: String, onReadMore .padding( vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16, - ), + ) + .testTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT), style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start), onClick = click, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 3c9d32501e..ac6ebfd1b3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -27,6 +28,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.feature.swap.models.states.PercentDifference @@ -117,7 +119,9 @@ private fun ProviderContentState( ) Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .testTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK), ) { Row { if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 2e2d7ab136..ada4986f2f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -36,6 +37,7 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.feature.swap.domain.models.ui.FeeType import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* @@ -275,7 +277,8 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { onClick = state.onChangeCardsClicked, indication = ripple(), interactionSource = remember { MutableInteractionSource() }, - ), + ) + .testTag(SwapTokenScreenTestTags.SWAP_BUTTON), ) { when (state.changeCardsButtonState) { ChangeCardsButtonState.UPDATE_IN_PROGRESS -> { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 81430c0cfa..8b8331fff7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -39,6 +40,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType @@ -181,7 +183,8 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie top = TangemTheme.dimens.spacing14, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, - ), + ) + .testTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -206,7 +209,8 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier - .align(Alignment.CenterVertically), + .align(Alignment.CenterVertically) + .testTag(SwapTokenScreenTestTags.BALANCE), ) } } else { @@ -254,7 +258,7 @@ private fun Content( color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h2, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), - modifier = sumTextModifier, + modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.RECEIVE_TEXT_FIELD), ) } else { RectangleShimmer( @@ -269,7 +273,7 @@ private fun Content( val focusRequester = remember { FocusRequester() } AutoSizeTextField( - modifier = sumTextModifier, + modifier = sumTextModifier.testTag(SwapTokenScreenTestTags.SWAP_TEXT_FIELD), focusRequester = focusRequester, textFieldValue = textFieldValue ?: TextFieldValue(), onAmountChange = { type.onAmountChanged(it) }, @@ -344,7 +348,8 @@ private fun Content( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing4) .width(TangemTheme.dimens.size40) - .height(TangemTheme.dimens.size12), + .height(TangemTheme.dimens.size12) + .testTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER), radius = TangemTheme.dimens.radius3, ) } @@ -365,7 +370,8 @@ fun Token( .padding( end = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing12, - ), + ) + .testTag(SwapTokenScreenTestTags.TOKEN), verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.End, ) { @@ -382,7 +388,9 @@ fun Token( maxLines = 1, style = TangemTheme.typography.subtitle2, textAlign = TextAlign.Center, - modifier = Modifier.defaultMinSize(minWidth = TangemTheme.dimens.size80), + modifier = Modifier + .defaultMinSize(minWidth = TangemTheme.dimens.size80) + .testTag(SwapTokenScreenTestTags.TOKEN_NAME), ) } } @@ -403,7 +411,8 @@ private fun TokenIcon( Box( modifier = Modifier .padding(end = TangemTheme.dimens.spacing16) - .size(TangemTheme.dimens.size42), + .size(TangemTheme.dimens.size42) + .testTag(SwapTokenScreenTestTags.TOKEN_ICON), ) { val tokenImageModifier = Modifier .align(Alignment.BottomStart) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt index 350dad39ff..e4b8b6ad00 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -18,6 +19,7 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState @@ -39,6 +41,7 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod text = state.name, style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.TOKEN_TITLE), ) NetworkInfoText(state.currency) } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index bac38b3402..91ee0ee9d8 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -49,7 +49,7 @@ internal enum class BuildType( BuildConfigField.Environment(value = "dev"), BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = true), - BuildConfigField.TesterMenuAvailability(isEnabled = false), + BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = true), ), ), From 89fbfa5971ec7011b0e504e3a3967411fc5ed1d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 13:04:19 +0300 Subject: [PATCH 077/165] Updated on 2026-08-14 --- .../di/UserWalletsListManagerModule.kt | 130 +++++-- .../DefaultUserWalletsListRepository.kt | 357 ++++++++++++++++++ .../UserWalletEncryptionKeysRepository.kt | 185 +++++++++ domain/core/build.gradle.kts | 1 + .../core/wallets/UserWalletsListRepository.kt | 132 +++++++ .../core/wallets/error}/DeleteWalletError.kt | 2 +- .../core/wallets/error/LockWalletsError.kt | 6 + .../core/wallets/error}/SaveWalletError.kt | 2 +- .../core/wallets/error/SelectWalletError.kt | 6 + .../domain/core/wallets/error/SetLockError.kt | 10 + .../core/wallets/error/UnlockWalletError.kt | 14 + domain/models/build.gradle.kts | 1 - .../wallets/hot/HotWalletPasswordRequester.kt | 2 + .../wallets/usecase/DeleteWalletUseCase.kt | 2 +- .../wallets/usecase/SaveWalletUseCase.kt | 2 +- .../features/details/utils/UserWalletSaver.kt | 2 +- .../DefaultHotAccessCodeRequestComponent.kt | 5 + .../proxy/HotWalletPasswordRequesterProxy.kt | 4 + .../setaccesscode/AccessCodeModel.kt | 2 +- gradle/tangem_dependencies.toml | 2 +- 20 files changed, 828 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt rename domain/{wallets/src/main/java/com/tangem/domain/wallets/models => core/src/main/kotlin/com/tangem/domain/core/wallets/error}/DeleteWalletError.kt (66%) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt rename domain/{wallets/src/main/java/com/tangem/domain/wallets/models => core/src/main/kotlin/com/tangem/domain/core/wallets/error}/SaveWalletError.kt (84%) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 86be1e5223..f6323a20de 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -11,14 +11,18 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager +import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository @@ -26,6 +30,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.tangemSdkManager import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -40,6 +45,7 @@ internal object UserWalletsListManagerModule { @Provides @Singleton + @Deprecated("Use UserWalletsListRepository instead") fun provideGeneralUserWalletsListManager( @ApplicationContext applicationContext: Context, appPreferencesStore: AppPreferencesStore, @@ -58,42 +64,14 @@ internal object UserWalletsListManagerModule { ) } + @Deprecated("Use UserWalletsListRepository instead") private fun createBiometricUserWalletsListManager( applicationContext: Context, analyticsEventHandler: AnalyticsEventHandler, dispatchers: CoroutineDispatcherProvider, ): UserWalletsListManager { - val moshi = Moshi.Builder() - .add(WalletDerivedKeysMapAdapter()) - .add(ScanResponseDerivedKeysMapAdapter()) - .add(ByteArrayKeyAdapter()) - .add(ExtendedPublicKeysMapAdapter()) - .add(CardBackupStatusAdapter()) - .add(DerivationPathAdapterWithMigration()) - .add(TangemSdkAdapter.DateAdapter()) - .add(TangemSdkAdapter.DerivationNodeAdapter()) - .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model - .add(VisaActivationRemoteState.jsonAdapter) - .add(VisaCardActivationStatus.jsonAdapter) - .addLast(KotlinJsonAdapterFactory()) - .build() - - val secureStorage = AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "user_wallets_storage", - ), - androidSecureStorageV2 = AndroidSecureStorageV2( - appContext = applicationContext, - useStrongBox = true, - name = "user_wallets_storage2", - ), - androidSecureStorageV3 = AndroidSecureStorageV2( - appContext = applicationContext, - useStrongBox = false, - name = "user_wallets_storage3", - ), - ) + val moshi = buildMoshi() + val secureStorage = buildSecureStorage(applicationContext = applicationContext) val authenticatedStorage = AuthenticatedStorage( secureStorage = UserWalletsKeysStoreDecorator( @@ -134,4 +112,94 @@ internal object UserWalletsListManagerModule { selectedUserWalletRepository = selectedUserWalletRepository, ) } + + @Provides + @Singleton + fun provideUserWalletsListRepository( + @ApplicationContext applicationContext: Context, + dispatchers: CoroutineDispatcherProvider, + passwordRequester: HotWalletPasswordRequester, + ): UserWalletsListRepository { + val moshi = buildMoshi() + val secureStorage = buildSecureStorage(applicationContext = applicationContext) + + val authenticatedStorage = AuthenticatedStorage( + secureStorage = UserWalletsKeysStoreDecorator( + featureStorage = secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, + ), + ) + + val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( + moshi = moshi, + secureStorage = secureStorage, + ) + + val selectedUserWalletRepository = DefaultSelectedUserWalletRepository( + secureStorage = secureStorage, + dispatchers = dispatchers, + ) + + val userWalletEncryptionKeysRepository = UserWalletEncryptionKeysRepository( + moshi = moshi, + authenticatedStorage = authenticatedStorage, + dispatchers = dispatchers, + secureStorage = secureStorage, + ) + + return DefaultUserWalletsListRepository( + publicInformationRepository = publicInformationRepository, + sensitiveInformationRepository = sensitiveInformationRepository, + selectedUserWalletRepository = selectedUserWalletRepository, + passwordRequester = passwordRequester, + userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository, + tangemSdkManagerProvider = Provider { tangemSdkManager }, + savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now + // TODO add a settings toggle to disable saving persistent information + ) + } + + fun buildMoshi(): Moshi { + return Moshi.Builder() + .add(WalletDerivedKeysMapAdapter()) + .add(ScanResponseDerivedKeysMapAdapter()) + .add(ByteArrayKeyAdapter()) + .add(ExtendedPublicKeysMapAdapter()) + .add(CardBackupStatusAdapter()) + .add(DerivationPathAdapterWithMigration()) + .add(TangemSdkAdapter.DateAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(VisaActivationRemoteState.jsonAdapter) + .add(VisaCardActivationStatus.jsonAdapter) + .addLast(KotlinJsonAdapterFactory()) + .build() + } + + fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage { + return AndroidSecureStorage( + preferences = SecureStorage.createEncryptedSharedPreferences( + context = applicationContext, + storageName = "user_wallets_storage", + ), + androidSecureStorageV2 = AndroidSecureStorageV2( + appContext = applicationContext, + useStrongBox = true, + name = "user_wallets_storage2", + ), + androidSecureStorageV3 = AndroidSecureStorageV2( + appContext = applicationContext, + useStrongBox = false, + name = "user_wallets_storage3", + ), + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt new file mode 100644 index 0000000000..d60a2a0f74 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -0,0 +1,357 @@ +package com.tangem.tap.domain.userWalletList.repository + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.either +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.flatMap +import com.tangem.common.map +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.wallets.R +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.core.wallets.error.DeleteWalletError +import com.tangem.domain.core.wallets.error.LockWalletsError +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError +import com.tangem.domain.core.wallets.error.SetLockError +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey +import com.tangem.tap.domain.userWalletList.utils.encryptionKey +import com.tangem.tap.domain.userWalletList.utils.lock +import com.tangem.tap.domain.userWalletList.utils.toUserWallets +import com.tangem.tap.domain.userWalletList.utils.updateWith +import com.tangem.utils.Provider +import com.tangem.utils.ProviderSuspend +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Suppress("LongParameterList") +internal class DefaultUserWalletsListRepository( + private val publicInformationRepository: UserWalletsPublicInformationRepository, + private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, + private val selectedUserWalletRepository: SelectedUserWalletRepository, + private val passwordRequester: HotWalletPasswordRequester, + private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository, + private val tangemSdkManagerProvider: Provider, + private val savePersistentInformation: ProviderSuspend, +) : UserWalletsListRepository { + + override val userWallets = MutableStateFlow?>(null) + override val selectedUserWallet = MutableStateFlow(null) + + override suspend fun load() { + if (userWallets.value != null) return + + if (savePersistentInformation().not()) { + // If we don't save persistent information, we don't need to load user wallets + // and we should clear any existing data + clearPersistentData() + userWallets.value = emptyList() + return + } + + val unsecuredEncryptionKeys = userWalletEncryptionKeysRepository.getAllUnsecured() + + publicInformationRepository.getAll() + .map { it.toUserWallets() } + .flatMap { wallets -> + sensitiveInformationRepository.getAll(unsecuredEncryptionKeys) + .map { wallets.updateWith(it) } + }.doOnSuccess { + userWallets.value = it + } + + val selectedUserWalletId = selectedUserWalletRepository.get() + selectedUserWallet.value = userWallets.value?.firstOrNull { it.walletId == selectedUserWalletId } + ?: userWallets.value?.firstOrNull() + } + + override suspend fun userWalletsSync(): List { + load() + return userWallets.value!! + } + + override suspend fun selectedUserWalletSync(): UserWallet? { + load() + return selectedUserWallet.value + } + + override suspend fun select(userWalletId: UserWalletId): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SelectWalletError.UnableToSelectUserWallet) + selectedUserWalletRepository.set(userWalletId) + selectedUserWallet.value = userWallet + userWallet + } + + override suspend fun saveWithoutLock( + userWallet: UserWallet, + canOverride: Boolean, + ): Either = either { + if (canOverride.not() && userWallets.value?.any { it.walletId == userWallet.walletId } == true) { + raise(SaveWalletError.WalletAlreadySaved(messageId = R.string.user_wallet_list_error_wallet_already_saved)) + } + + if (savePersistentInformation()) { + publicInformationRepository.save(userWallet, canOverride) + if (userWallet.isLocked.not()) { + sensitiveInformationRepository.save(userWallet, userWallet.encryptionKey) + } + } + + // update the userWallets state and add if it doesn't exist + userWallets.update { currentWallets -> + val wallets = currentWallets ?: emptyList() + if (wallets.any { it.walletId == userWallet.walletId }) { + wallets.map { if (it.walletId == userWallet.walletId) userWallet else it } + } else { + wallets + userWallet + } + } + + // update the selectedUserWallet state if it is the only wallet + if (userWallets.value?.size == 1) { + selectedUserWalletRepository.set(userWallet.walletId) + selectedUserWallet.value = userWallet + } + + userWallet + } + + override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either = + either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SetLockError.UserWalletNotFound) + + val encryptionKey = userWallet.encryptionKey + ?: raise(SetLockError.UserWalletLocked) + + runCatching { + userWalletEncryptionKeysRepository.save( + encryptionKey = UserWalletEncryptionKey( + walletId = userWalletId, + encryptionKey = encryptionKey, + ), + method = when (lockMethod) { + is LockMethod.AccessCode -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + } + LockMethod.Biometric -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric + } + LockMethod.NoLock -> { + if (userWallet is UserWallet.Cold) { + raise(SetLockError.UserWalletNotFound) + } + + UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured + } + }, + ) + }.onFailure { raise(SetLockError.UnableToSetLock(it)) } + } + + override suspend fun delete(userWalletIds: List): Either = either { + if (userWalletIds.isEmpty()) return Unit.right() + + publicInformationRepository.delete(userWalletIds) + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } + sensitiveInformationRepository.delete(userWalletIds) + .doOnFailure { + raise(DeleteWalletError.UnableToDelete) + } + + userWalletEncryptionKeysRepository.delete(userWalletIds) + + userWallets.update { currentWallets -> + currentWallets?.filterNot { it.walletId in userWalletIds } + } + + selectedUserWallet.update { currentSelected -> + if (currentSelected == null) return@update null + + userWallets.value?.findAvailableUserWallet( + userWallets.value?.indexOfFirst { it.walletId == currentSelected.walletId } ?: 0, + ) + } + } + + override suspend fun unlock( + userWalletId: UserWalletId, + unlockMethod: UserWalletsListRepository.UnlockMethod, + ): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(UnlockWalletError.UserWalletNotFound) + + if (userWallet.isLocked.not()) { + raise(UnlockWalletError.AlreadyUnlocked) + } + + when (unlockMethod) { + UserWalletsListRepository.UnlockMethod.Biometric -> { + unlockAllWallets() + select(userWalletId) + } + UserWalletsListRepository.UnlockMethod.AccessCode -> { + if (userWallet !is UserWallet.Hot) { + raise(UnlockWalletError.UnableToUnlock) + } + + val encryptionKey = requestPasswordRecursive( + block = { password -> + runCatching { + userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) + }.onFailure { + raise(UnlockWalletError.UnableToUnlock) + }.getOrNull() + }, + biometryFallback = { + unlock(userWalletId, UserWalletsListRepository.UnlockMethod.Biometric) + }, + ).bind() + + if (encryptionKey == null) { + return@either + } + + sensitiveInformationRepository.getAll(listOf(encryptionKey)) + .doOnSuccess { userWallets.value?.updateWith(it) } + .doOnFailure { error -> + raise(UnlockWalletError.UnableToUnlock) + } + } + UserWalletsListRepository.UnlockMethod.Scan -> { + if (userWallet !is UserWallet.Cold) { + raise(UnlockWalletError.UnableToUnlock) + } + + tangemSdkManagerProvider().scanProduct() + .doOnSuccess { scanResponse -> + val expectedId = UserWalletIdBuilder.scanResponse(scanResponse).build() + + if (expectedId != userWallet.walletId) { + raise(UnlockWalletError.ScannedCardWalletNotMatched) + } + + saveWithoutLock(userWallet.copy(scanResponse = scanResponse), canOverride = true) + .mapLeft { UnlockWalletError.UnableToUnlock } + .bind() + } + .doOnFailure { + raise(UnlockWalletError.UserCancelled) + } + } + } + } + + override suspend fun unlockAllWallets(): Either = either { + val biometricKeys = runCatching { + userWalletEncryptionKeysRepository.getAllBiometric() + }.getOrElse { + // TODO handle error properly [REDACTED_TASK_KEY] + raise(UnlockWalletError.UserCancelled) + } + + val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() + val allKeys = biometricKeys + unsecuredKeys + sensitiveInformationRepository.getAll(allKeys) + .doOnSuccess { userWallets.value?.updateWith(it) } + } + + override suspend fun lockAllWallets(): Either = either { + val unsecuredWalletIds = userWalletEncryptionKeysRepository.getAllUnsecured().map { it.walletId }.toSet() + + if (unsecuredWalletIds.size == userWallets.value?.size) { + raise(LockWalletsError.NothingToLock) + } + + userWallets.update { + it?.map { + if (it.walletId !in unsecuredWalletIds) { + it.lock() + } else { + it + } + } + } + } + + override suspend fun clearPersistentData() { + publicInformationRepository.clear() + sensitiveInformationRepository.clear() + userWalletEncryptionKeysRepository.clear() + } + + private suspend fun requestPasswordRecursive( + block: suspend (CharArray) -> UserWalletEncryptionKey?, + biometryFallback: suspend () -> Either, + ): Either { + val result = passwordRequester.requestPassword( + hasBiometry = tangemSdkManagerProvider.invoke().needEnrollBiometrics, + ) + + return when (result) { + HotWalletPasswordRequester.Result.Dismiss -> { + passwordRequester.dismiss() + UnlockWalletError.UserCancelled.left() + } + is HotWalletPasswordRequester.Result.EnteredPassword -> { + val decrypted = block(result.password.value) + if (decrypted == null) { + passwordRequester.wrongPassword() + requestPasswordRecursive(block, biometryFallback) + } else { + passwordRequester.successfulAuthentication() + decrypted.right() + } + } + HotWalletPasswordRequester.Result.UseBiometry -> { + biometryFallback() + .onRight { + passwordRequester.successfulAuthentication() + } + passwordRequester.dismiss() + null.right() + } + } + } + + /** + * Find the nearest available wallet that can be selected + * + * Example: + * Number with *n* is previous selected wallet with index [prevSelectedIndex]. + * + * 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4] + * 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4] + * 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*] + * 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*] + * + * @receiver list of user wallets without deleted wallet + */ + private fun List.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? { + if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull() + + if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex] + + for (offset in 1..size) { + val rightIndex = prevSelectedIndex + offset + if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex] + + val leftIndex = prevSelectedIndex - offset + if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex] + } + + return lastOrNull() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt new file mode 100644 index 0000000000..fb42970c29 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -0,0 +1,185 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.common.authentication.storage.AuthenticatedStorage +import com.tangem.common.services.secure.SecureStorage +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.hot.sdk.android.crypto.AESEncryptionProtocol +import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class UserWalletEncryptionKeysRepository( + moshi: Moshi, + private val authenticatedStorage: AuthenticatedStorage, + private val dispatchers: CoroutineDispatcherProvider, + private val secureStorage: SecureStorage, +) { + + private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( + UserWalletEncryptionKey::class.java, + ) + private val userWalletsIdsListAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, UserWalletId::class.java), + ) + + suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) { + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + + when (method) { + EncryptionMethod.Unsecured -> { + secureStorage.store( + account = StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } + EncryptionMethod.Biometric -> { + authenticatedStorage.store( + keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } + is EncryptionMethod.Password -> { + val encodedWithPass = AESEncryptionProtocol.encryptWithPassword( + password = method.password, + content = encryptionKey.encode(), + ) + secureStorage.store( + account = StorageKey.UserWalletEncryptionKeyEncrypted(encryptionKey.walletId).name, + data = encodedWithPass, + ) + } + } + + storeUserWalletId(userWalletId = encryptionKey.walletId) + } + + suspend fun getAllUnsecured(): List = withContext(dispatchers.io) { + getUserWalletsIds().mapNotNull { userWalletId -> + secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey() + } + } + + suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? { + val encrypted = secureStorage.get( + account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, + ) ?: return null + + val decrypted = AESEncryptionProtocol.decryptWithPassword(password, encrypted) + + return decrypted.decodeToKey() + } + + suspend fun getAllBiometric(): List = withContext(dispatchers.io) { + val keys = getUserWalletsIds().map { userWalletId -> + StorageKey.UserWalletEncryptionKey(userWalletId).name + } + + authenticatedStorage.get(keys).mapNotNull { + it.value.decodeToKey() + } + } + + suspend fun delete(userWalletIds: List) { + if (userWalletIds.isEmpty()) return + + withContext(dispatchers.io) { + userWalletIds.forEach { userWalletId -> + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name) + secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name) + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + + val userWalletsIds = getUserWalletsIds().filterNot { it in userWalletIds } + secureStorage.store(userWalletsIds.encode(), StorageKey.UserWalletIds.name) + } + } + + suspend fun clear() { + withContext(dispatchers.io) { + val userWalletsIds = getUserWalletsIds() + userWalletsIds.forEach { userWalletId -> + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name) + secureStorage.delete(StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name) + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + secureStorage.delete(StorageKey.UserWalletIds.name) + } + } + + private suspend fun getUserWalletsIds(): List { + return withContext(dispatchers.io) { + secureStorage.get(StorageKey.UserWalletIds.name) + .decodeToUserWalletsIds() + } + } + + private suspend fun storeUserWalletId(userWalletId: UserWalletId) { + val userWalletIds = (getUserWalletsIds() + userWalletId).distinct() + + withContext(dispatchers.io) { + secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name) + } + } + + private suspend fun UserWalletEncryptionKey.encode(): ByteArray { + return withContext(dispatchers.default) { + this@encode + .let(encryptionKeyAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? { + return withContext(dispatchers.default) { + this@decodeToKey + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(encryptionKeyAdapter::fromJson) + } + } + + private suspend fun List.encode(): ByteArray { + return withContext(dispatchers.default) { + this@encode + .let(userWalletsIdsListAdapter::toJson) + .encodeToByteArray(throwOnInvalidSequence = true) + } + } + + private suspend fun ByteArray?.decodeToUserWalletsIds(): List { + return withContext(dispatchers.default) { + this@decodeToUserWalletsIds + ?.decodeToString(throwOnInvalidSequence = true) + ?.let(userWalletsIdsListAdapter::fromJson) + .orEmpty() + } + } + + sealed class EncryptionMethod { + data object Unsecured : EncryptionMethod() + data object Biometric : EncryptionMethod() + class Password(val password: CharArray) : EncryptionMethod() + } + + private sealed interface StorageKey { + val name: String + + class UserWalletEncryptionKeyUnsecured(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_unsecured_${userWalletId.stringValue}" + } + + class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}" + } + + class UserWalletEncryptionKeyEncrypted(userWalletId: UserWalletId) : StorageKey { + override val name: String = "user_wallet_encryption_key_encrypted_${userWalletId.stringValue}" + } + + object UserWalletIds : StorageKey { + override val name: String = "user_wallets_ids_with_saved_keys" + } + } +} \ No newline at end of file diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 9910d828a3..20e637a0a5 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { api(deps.kotlin.coroutines) api(deps.arrow.core) api(deps.arrow.fx) + api(projects.domain.models) implementation(deps.kotlin.serialization) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt new file mode 100644 index 0000000000..f0f9ee988a --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -0,0 +1,132 @@ +package com.tangem.domain.core.wallets + +import arrow.core.Either +import com.tangem.domain.core.wallets.error.DeleteWalletError +import com.tangem.domain.core.wallets.error.LockWalletsError +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError +import com.tangem.domain.core.wallets.error.SetLockError +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.StateFlow + +/** + * Repository for managing user wallets list. + * It provides methods to load, select, save, lock, unlock, and delete user wallets. + * + * TODO tests [REDACTED_TASK_KEY] + * + * @see com.tangem.domain.models.wallet.UserWallet + * @see com.tangem.domain.models.wallet.UserWalletId + */ +interface UserWalletsListRepository { + + /** + * List of user wallets. + * It can be null if the list is not loaded yet. + */ + val userWallets: StateFlow?> + + /** + * Currently selected user wallet. + * It can be null if wallets list is not loaded yet or wallets list is empty. + */ + val selectedUserWallet: StateFlow + + /** + * Loads user wallets list and selected wallet. + * If the list is already loaded, it does nothing. + */ + suspend fun load() + + /** + * Gets and if necessary loads user wallets list and selected wallet. + */ + suspend fun userWalletsSync(): List + + /** + * Gets and if necessary loads selected user wallet. + */ + suspend fun selectedUserWalletSync(): UserWallet? + + /** + * Selects user wallet by id. + * If the wallet is not found, it returns [SelectWalletError.UnableToSelectUserWallet]. + */ + suspend fun select(userWalletId: UserWalletId): Either + + /** + * Saves user wallet. + * If the wallet already exists and [canOverride] is false, it returns [SaveWalletError.WalletAlreadySaved]. + * If the wallet already exists and [canOverride] is true, it overrides the existing wallet. + * + * Does not lock the wallet after saving, it should be done manually using [setLock] method. + */ + suspend fun saveWithoutLock( + userWallet: UserWallet, + canOverride: Boolean = true, + ): Either + + /** + * Sets lock for **unlocked** user wallet. + * If the wallet is not found, it returns [SetLockError.UserWalletNotFound] + * If the wallet is locked, it returns [SetLockError.UserWalletLocked] + * If the lock method is not supported, it returns [SetLockError.UnableToSetLock]. + */ + suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either + + /** + * Deletes user wallets by ids. + * If the wallet is not found, it returns [DeleteWalletError.UnableToDelete] + */ + suspend fun delete(userWalletIds: List): Either + + /** + * Unlocks specific user wallet. + * If the wallet is already unlocked, returns [UnlockWalletError.AlreadyUnlocked]. + * If the wallet is not found, returns [UnlockWalletError.UserWalletNotFound]. + * If the unlock method is not supported, returns [UnlockWalletError.UnableToUnlock] + * If the user cancels the unlock operation (ex. dismisses dialogs), returns [UnlockWalletError.UserCancelled]. + * If the scanned card does not match the wallet, returns [UnlockWalletError.ScannedCardWalletNotMatched]. + */ + suspend fun unlock(userWalletId: UserWalletId, unlockMethod: UnlockMethod): Either + + /** + * Unlocks all user wallets using biometric authentication. + * If all the wallets was are already unlocked, returns [UnlockWalletError.AlreadyUnlocked]. + * Success if at least one wallet was unlocked. + * If the biometric method is not supported for some of user wallets, returns [UnlockWalletError.UnableToUnlock] + */ + suspend fun unlockAllWallets(): Either + + /** + * Locks all secured user wallets (wallets that are not locked with [LockMethod.NoLock]). + * If all the wallets are already locked or unsecured, returns [LockWalletsError.NothingToLock]. + * Success if at least one wallet was locked. + */ + suspend fun lockAllWallets(): Either + + /** + * Clears all persistent data related to user wallets. + * This includes removing all user wallets, selected wallet, and any other related data. + * User wallets will stay in the cache, but will be reloaded on next repository initialization. + */ + suspend fun clearPersistentData() + + sealed class LockMethod { + data object Biometric : LockMethod() + class AccessCode(val accessCode: CharArray) : LockMethod() + data object NoLock : LockMethod() + } + + enum class UnlockMethod { + Biometric, + AccessCode, + Scan, + } +} + +fun UserWalletsListRepository.requireUserWalletsSync(): List { + return userWallets.value ?: error("User wallets list is not loaded") +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt similarity index 66% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt rename to domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt index d58c220ac3..91b685a84c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/DeleteWalletError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/DeleteWalletError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.wallets.models +package com.tangem.domain.core.wallets.error sealed interface DeleteWalletError { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt new file mode 100644 index 0000000000..abedc5aa4a --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/LockWalletsError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.wallets.error + +interface LockWalletsError { + + data object NothingToLock : LockWalletsError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt similarity index 84% rename from domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt rename to domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt index a41524b8b7..30fc8a743b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveWalletError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.wallets.models +package com.tangem.domain.core.wallets.error /** [REDACTED_AUTHOR] diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt new file mode 100644 index 0000000000..05b07e53d0 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SelectWalletError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SelectWalletError { + + data object UnableToSelectUserWallet : SelectWalletError +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt new file mode 100644 index 0000000000..688f618c74 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SetLockError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SetLockError { + + data object UserWalletNotFound : SetLockError + + data object UserWalletLocked : SetLockError + + data class UnableToSetLock(val cause: Throwable) : SetLockError +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt new file mode 100644 index 0000000000..8bed3120a7 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/UnlockWalletError.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.core.wallets.error + +sealed interface UnlockWalletError { + + data object AlreadyUnlocked : UnlockWalletError + + data object UserWalletNotFound : UnlockWalletError + + data object UnableToUnlock : UnlockWalletError + + data object UserCancelled : UnlockWalletError + + data object ScannedCardWalletNotMatched : UnlockWalletError +} \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index 9bd4e4940a..6c9f6b1143 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -12,7 +12,6 @@ tasks.withType().configureEach { } dependencies { - api(projects.domain.core) api(projects.domain.visa.models) api(projects.core.utils) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index 6f8391e887..7dcc5fa579 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -6,6 +6,8 @@ interface HotWalletPasswordRequester { suspend fun wrongPassword() + suspend fun successfulAuthentication() + suspend fun requestPassword(hasBiometry: Boolean): Result suspend fun dismiss() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index 65faf81196..c876b7d526 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.raise.either import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.DeleteWalletError +import com.tangem.domain.core.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId /** diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 66777ba22b..9ff8c5bb81 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet /** diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 10c6667755..7915a2a320 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -21,7 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index f23af23895..5bacfc9fef 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -25,6 +25,11 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( model.wrongAccessCode() } + override suspend fun successfulAuthentication() { + // TODO handle successful authentication + // TODO add delay + } + override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { model.show(hasBiometry) return model.waitResult() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index 5208d91418..b7e7218a48 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -17,6 +17,10 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR call { wrongPassword() } } + override suspend fun successfulAuthentication() { + call { successfulAuthentication() } + } + override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result = call { requestPassword(hasBiometry) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt index 75210c3f64..e387f3e404 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt @@ -83,7 +83,7 @@ internal class AccessCodeModel @Inject constructor( unlockHotWallet = unlockHotWallet, auth = HotAuth.Password(accessCode.toCharArray()), ) - saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId)) + saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true) params.callbacks.onAccessCodeConfirmed(params.userWalletId) } }.onFailure { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e880536eea..5f56886fc4 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-446" +tangemHotSdk = "develop-448" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 934b6ed6c60b806786a43b6255d157a7803a300e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 15:16:56 +0300 Subject: [PATCH 078/165] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../moonpay/MoonpayBlockchainMapping.kt | 1 + .../core/ui/extensions/BlockchainIcons.kt | 3 +++ .../main/res/drawable/ic_hyperliquid_22.xml | 9 +++++++++ .../main/res/drawable/img_hyperliquid_22.xml | 19 +++++++++++++++++++ .../data/common/network/NetworkFactory.kt | 1 + .../legacy/MercuryoBlockchainMapping.kt | 1 + .../domain/card/configs/Wallet2CardConfig.kt | 2 ++ .../card/configs/Wallet2CardConfigTest.kt | 2 ++ gradle/tangem_dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ 11 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_hyperliquid_22.xml create mode 100644 core/ui/src/main/res/drawable/img_hyperliquid_22.xml diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index e87768263c..23aae9e349 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit e87768263c0a79018958b0872940c604180a623a +Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193 diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index 235cf6be4c..14757eae60 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -158,4 +158,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? ZkLinkNova, ZkLinkNovaTestnet -> null KaspaTestnet -> null Pepecoin, PepecoinTestnet -> null + Hyperliquid, HyperliquidTestnet -> null } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index eb7f85d306..87c7602ba9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -94,6 +94,7 @@ fun getActiveIconRes(blockchainId: String): Int { "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 else -> R.drawable.ic_alert_24 } } @@ -186,6 +187,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 else -> R.drawable.ic_alert_24 } } @@ -281,6 +283,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "zklink", "zklink/test" -> R.drawable.ic_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22 "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 + "hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml b/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml new file mode 100644 index 0000000000..7f7c511fa5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hyperliquid_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_hyperliquid_22.xml b/core/ui/src/main/res/drawable/img_hyperliquid_22.xml new file mode 100644 index 0000000000..9e66c2bd9a --- /dev/null +++ b/core/ui/src/main/res/drawable/img_hyperliquid_22.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index c05f6ea576..9a57b58ea2 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -324,6 +324,7 @@ class NetworkFactory @Inject constructor( Blockchain.Scroll, Blockchain.ScrollTestnet, Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet, Blockchain.Pepecoin, Blockchain.PepecoinTestnet, + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index 2291dbc4c6..486ba69104 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -158,5 +158,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> null Blockchain.KaspaTestnet -> null Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 346a09dc81..58c029cccb 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -207,6 +207,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.ZkLinkNovaTestnet -> EllipticCurve.Secp256k1 Blockchain.Pepecoin -> EllipticCurve.Secp256k1 Blockchain.PepecoinTestnet -> EllipticCurve.Secp256k1 + Blockchain.Hyperliquid -> EllipticCurve.Secp256k1 + Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt index 152e79a017..927e7f0aa1 100644 --- a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt +++ b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt @@ -164,6 +164,8 @@ class Wallet2CardConfigTest { Blockchain.ZkLinkNovaTestnet to EllipticCurve.Secp256k1, Blockchain.Pepecoin to EllipticCurve.Secp256k1, Blockchain.PepecoinTestnet to EllipticCurve.Secp256k1, + Blockchain.Hyperliquid to EllipticCurve.Secp256k1, + Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 5f56886fc4..2c35b41fa5 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1133" +tangemBlockchainSdk = "develop-1134" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-509" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index c7512e7946..34d846e753 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -165,6 +165,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "zklink/test" -> Blockchain.ZkLinkNovaTestnet "pepecoin" -> Blockchain.Pepecoin "pepecoin/test" -> Blockchain.PepecoinTestnet + "hyperliquid" -> Blockchain.Hyperliquid + "hyperliquid/test" -> Blockchain.HyperliquidTestnet else -> null } } @@ -327,6 +329,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ZkLinkNovaTestnet -> "zklink/test" Blockchain.Pepecoin -> "pepecoin" Blockchain.PepecoinTestnet -> "pepecoin/test" + Blockchain.Hyperliquid -> "hyperliquid" + Blockchain.HyperliquidTestnet -> "hyperliquid/test" } } @@ -430,6 +434,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll-ethereum" Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink-ethereum" Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> "hyperliquid" } } From 2a044a55823b996aec6856d295fa5b9f26f7d5e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 13:43:39 +0400 Subject: [PATCH 079/165] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 6 +- .../usecase/ArchiveCryptoPortfolioUseCase.kt | 92 +++++++++- .../ArchiveCryptoPortfolioUseCaseTest.kt | 157 ++++++++++++++++++ 3 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index c4f9660136..41d100f879 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -31,8 +31,10 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideArchiveCryptoPortfolioUseCase(): ArchiveCryptoPortfolioUseCase { - return ArchiveCryptoPortfolioUseCase() + fun provideArchiveCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): ArchiveCryptoPortfolioUseCase { + return ArchiveCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } @Provides diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt index 8e473b507d..0611b106fb 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -1,22 +1,102 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId /** + * Use case for archiving a crypto portfolio. + * This class provides functionality to archive a specific account within a user's crypto portfolio. + * It ensures that the account exists and meets the necessary requirements before performing the operation. + * + * @property crudRepository repository for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class ArchiveCryptoPortfolioUseCase { +class ArchiveCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** Archives the specified account by its [accountId] */ suspend operator fun invoke(accountId: AccountId): Either = either { - // TODO: [REDACTED_JIRA] - // Remove the account from the list of active accounts - // Add the account to the list of archived accounts - // Save to backend + val accountList = getAccountList(userWalletId = accountId.userWalletId) + + val archivingAccount = accountList.accounts + .firstOrNull { it.accountId == accountId } + ?: raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + + val updatedAccounts = (accountList - archivingAccount).getOrElse { + raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) + } + + saveAccounts(updatedAccounts) } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the archiving process + */ sealed interface Error { - data object DataOperationFailed : Error + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$this: Data operation failed: ${cause.message ?: "Unknown error"}" + } + + /** + * Represents critical technical errors that can occur during the update operation. + * These errors are a consequence of an inconsistent state. + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + + override fun toString(): String { + return "${this.javaClass.simpleName}: Accounts for $userWalletId are not created" + } + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "${this.javaClass.simpleName}: Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : CriticalTechError { + + override fun toString(): String { + return "${this.javaClass.simpleName}: Account list requirements not met: $cause" + } + } + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..aaea0a5379 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,157 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchiveCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository) + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should archive existing crypto portfolio account`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountId = account.accountId + + val archivedAccount = account.copy(isArchived = true) + val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Unit.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should return error if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if account not found`() = runTest { + // Arrange + val accountList = AccountList.empty(userWallet) + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex(1).getOrNull()!!, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountId = account.accountId + + val archivedAccount = account.copy(isArchived = true) + val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file From 458fe22b92d53af7980b907e6e1b694786c1f87a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 16:47:17 +0300 Subject: [PATCH 080/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 2c35b41fa5..b1088cbc11 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1134" +tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-509" +tangemCardSdk = "develop-511" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-448" +tangemHotSdk = "develop-454" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 7d38ec51cced200d43d414ce3556cc4dc8b24b68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 19:58:05 +0500 Subject: [PATCH 081/165] Updated on 2026-08-14 --- .../swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index 06fb3dc71b..ce36cd1c38 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -9,7 +9,7 @@ import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.utils.extensions.isZero +import com.tangem.utils.isNullOrZero import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.min @@ -32,12 +32,11 @@ internal object SwapAmountQuoteUtils { secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate } + if (fromRate.isNullOrZero() || toRate.isNullOrZero()) return null + val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) val toTokenFiatValue = toTokenAmount.multiply(toRate) - // Check for zero division - if (fromTokenFiatValue.isZero() || toTokenFiatValue.isZero()) return null - val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf { From e12ec65fec96f21c0dc878486a1f46d891a6296e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 19:58:22 +0500 Subject: [PATCH 082/165] Updated on 2026-08-14 --- .../transaction/usecase/ValidateTransactionUseCase.kt | 6 +++--- .../sendviaswap/confirm/SendWithSwapConfirmComponent.kt | 6 +++++- .../sendviaswap/confirm/model/SendWithSwapConfirmModel.kt | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt index 7ade9be75d..499e222199 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -6,8 +6,8 @@ import arrow.core.right import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.network.Network -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.TransactionRepository class ValidateTransactionUseCase( private val transactionRepository: TransactionRepository, @@ -21,8 +21,8 @@ class ValidateTransactionUseCase( destination: String, userWalletId: UserWalletId, network: Network, - ): Either { - return transactionRepository.validateTransaction( + ): Either = Either.catch { + transactionRepository.validateTransaction( amount = amount, fee = fee, memo = memo, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 94f6d53022..b0cfea80d2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapDirection @@ -104,7 +105,10 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( appCurrency = params.appCurrency, callback = model, notificationData = SendNotificationsComponent.Params.NotificationData( - destinationAddress = model.confirmData.enteredDestination.orEmpty(), + destinationAddress = when (val currency = model.primaryCurrencyStatus.currency) { + is CryptoCurrency.Token -> currency.contractAddress + is CryptoCurrency.Coin -> "0" + }, memo = null, amountValue = model.confirmData.enteredAmount.orZero(), reduceAmountBy = model.confirmData.reduceAmountBy.orZero(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 5ebe7db905..a5aa42ce31 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -329,7 +329,10 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { sendNotificationsUpdateTrigger.triggerUpdate( data = NotificationData( - destinationAddress = confirmData.enteredDestination.orEmpty(), + destinationAddress = when (val currency = primaryCurrencyStatus.currency) { + is CryptoCurrency.Token -> currency.contractAddress + is CryptoCurrency.Coin -> "0" + }, memo = null, amountValue = confirmData.enteredAmount.orZero(), reduceAmountBy = confirmData.reduceAmountBy, From 696a410499eb8a618b721ba311075c9641ebfff0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 15:08:15 +0400 Subject: [PATCH 083/165] Updated on 2026-08-14 --- build.gradle.kts | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4cbeef6686..c490aef8c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -96,24 +96,21 @@ val generateComposeMetrics by tasks.registering { description = "Build external APK and generates compose metrics to 'build/compose-metrics' directory" subprojects { - tasks.withType { - compilerOptions { + tasks.withType().configureEach { + if (name.contains("compile")) { val outputDirectory = "${project.buildDir.absolutePath}/compose_metrics" - // Metrics - freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$outputDirectory", - ) - // Reports - freeCompilerArgs.addAll( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory", - ) - // Compose strong skipping mode - // freeCompilerArgs.addAll( - // "-P", - // "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", - // ) + compilerOptions { + freeCompilerArgs.addAll( + listOf( + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=$outputDirectory", + "-P", + "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=$outputDirectory" + // "-P", + // "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true", + ) + ) + } } } } From 517f260a70a6a2600623c685cc2ef70e27334748 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 14:31:38 +0500 Subject: [PATCH 084/165] Updated on 2026-08-14 --- .../ui/SwapChooseTokenNetworkContent.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index d2c33cddad..b22718a200 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -96,9 +96,12 @@ internal fun SwapChooseTokenNetworkContent(state: SwapChooseTokenNetworkContentU @Composable private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList) { Column( - modifier = Modifier - .padding(top = 8.dp) - .padding(16.dp), + modifier = Modifier.padding( + top = 8.dp, + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) { swapNetworks.fastForEachIndexed { index, network -> Row( @@ -116,10 +119,7 @@ private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList Date: Tue, 12 Aug 2025 14:32:18 +0500 Subject: [PATCH 085/165] Updated on 2026-08-14 --- .../domain/swap/usecase/GetSwapSupportedPairsUseCase.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index b470cc5a4a..6ef54ca2bd 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -67,7 +67,9 @@ class GetSwapSupportedPairsUseCase( // Search available to swap currency .filter { pair -> cryptoCurrencyList.any { currencyStatus -> - currencyStatus.id == pair.to.currency.id + // Allowed only on networks without tx extras (e.i. memo and destination tag) + val isExtrasSupported = currencyStatus.network.transactionExtrasType.isTxExtrasSupported() + currencyStatus.id == pair.to.currency.id && !isExtrasSupported } }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } From b6608d83784e46dbdd6c7d667a95276f832aed9d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 14:33:25 +0500 Subject: [PATCH 086/165] Updated on 2026-08-14 --- .../v2/impl/amount/model/SwapAmountModel.kt | 17 +++--- .../impl/amount/model/SwapAmountQuoteUtils.kt | 11 ++-- .../SwapAmountValueChangeTransformer.kt | 54 ++++++++++--------- .../SwapQuoteEmptyStateTransformer.kt | 17 ++++++ 4 files changed, 63 insertions(+), 36 deletions(-) create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index fb4748b307..4512f765ac 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -22,11 +22,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.swap.models.SwapCurrencies -import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.models.SwapQuoteModel @@ -56,8 +56,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler -import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero +import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.update import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -199,8 +199,6 @@ internal class SwapAmountModel @Inject constructor( ) amountDebouncer.debounce( coroutineScope = modelScope, - waitMs = DEBOUNCE_AMOUNT_DELAY, - forceUpdate = true, destinationFunction = { startLoadingQuotesTask(isSilentReload = false) }, @@ -532,11 +530,12 @@ internal class SwapAmountModel @Inject constructor( SwapDirection.Reverse -> state.secondaryAmount.amountField } as? AmountState.Data - if (fromAmount?.amountTextField?.isError == true) return + val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value ?: return - - if (fromAmountValue.isZero()) return + if (fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero()) { + uiState.transformerUpdate(SwapQuoteEmptyStateTransformer) + return + } val swapGroups = state.swapCurrencies.getGroupWithDirection(state.swapDirection) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index 06fb3dc71b..d696518b75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -56,15 +56,20 @@ internal object SwapAmountQuoteUtils { ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this - return if ( + val updatedAmountField = if ( selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct ) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(primaryAmount = amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus)) + amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus) } else { if (secondaryCryptoCurrencyStatus == null) return this val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(secondaryAmount = amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus)) + amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus) } + + return copy( + isPrimaryButtonEnabled = updatedAmountField.amountField.isPrimaryButtonEnabled, + primaryAmount = updatedAmountField, + ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 5a7e9eccb3..c364fd36a0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -17,33 +17,39 @@ internal class SwapAmountValueChangeTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - return prevState - .copy(selectedQuote = SwapQuoteUM.Loading) - .updateAmount( - onPrimaryAmount = { primaryStatus -> + val updatedState = prevState.updateAmount( + onPrimaryAmount = { primaryStatus -> + copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = primaryStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = value, + ).transform(prevState.primaryAmount.amountField), + ) + }, + onSecondaryAmount = { secondaryStatus -> + if (secondaryMaximumAmountBoundary != null) { copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = primaryStatus, - maxEnterAmount = primaryMaximumAmountBoundary, - minimumTransactionAmount = primaryMinimumAmountBoundary, + cryptoCurrencyStatus = secondaryStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, value = value, - ).transform(prevState.primaryAmount.amountField), + ).transform(prevState.secondaryAmount.amountField), ) - }, - onSecondaryAmount = { secondaryStatus -> - if (secondaryMaximumAmountBoundary != null) { - copy( - amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = secondaryStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = value, - ).transform(prevState.secondaryAmount.amountField), - ) - } else { - this - } - }, - ) + } else { + this + } + }, + ) + + return (updatedState as? SwapAmountUM.Content)?.copy( + selectedQuote = if (updatedState.isPrimaryButtonEnabled) { + SwapQuoteUM.Empty + } else { + SwapQuoteUM.Loading + }, + ) ?: updatedState } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt new file mode 100644 index 0000000000..32cd5ea158 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapQuoteEmptyStateTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.transformer.Transformer + +internal object SwapQuoteEmptyStateTransformer : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + return prevState.copy( + selectedQuote = SwapQuoteUM.Empty, + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file From 410e4a9593dbe05123fc0c98094b8d3f413f46f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 14:34:28 +0500 Subject: [PATCH 087/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 5 +++ core/res/src/main/res/values-es/strings.xml | 37 ++++++++++++++++++- core/res/src/main/res/values-fr/strings.xml | 5 +++ core/res/src/main/res/values-it/strings.xml | 5 +++ core/res/src/main/res/values-ja/strings.xml | 21 ++++++++++- core/res/src/main/res/values-ru/strings.xml | 19 ++++++++++ .../src/main/res/values-uk-rUA/strings.xml | 7 ++++ .../src/main/res/values-zh-rTW/strings.xml | 5 +++ core/res/src/main/res/values/strings.xml | 26 ++++++++++--- 9 files changed, 122 insertions(+), 8 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 673c284024..2cbf9e75ad 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -147,6 +147,7 @@ Schließen Demnächst verfügbar Bestätigen + Verbinden Kontakt zum Tangem-Support Kontakt zum Visa-Support Weiter @@ -249,6 +250,10 @@ Allgemeine Geschäftsbedingungen Nutzungsbedingungen Heute + + %d Token + %d Token + Transaktion fehlgeschlagen Transaktionsstatus Transaktionen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 40a09f5b5a..f134baf614 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1,5 +1,9 @@ + Archivar cuenta + Archivo + Estás archivando esta cuenta, pero siempre puedes recuperarla. + Cuenta ¿No encuentra el token en su billetera? Consulte la sección de Mercados para encontrarlo y añadirlo a la compra ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta Vender @@ -34,12 +38,16 @@ Esta tarjeta no admite tokens en la red %1$s debido a una limitación del firmware. ¿Tiene dificultades para escanear su tarjeta/anillo? Esta tarjeta no está diseñada para funcionar con Tangem + Utilice %1$s para desbloquear de forma rápida y segura su biletera y autorizar todas las acciones importantes, como la firma de transacciones. En el caso de las billeteras físicas, seguirá necesitando una tarjeta para firmar. Tarifa por defecto Habilite las Tarifas predeterminadas para establecer automáticamente las tarifas de transacción y omitir la página de Tarifas al enviar fondos. Siempre puede volver a esta página si es necesario. Vaya a ajustes para habilitar la autenticación biométrica en la Tangem App Habilitar autenticación biométrica + Para deshabilitar %1$s deberá ingresar su código de acceso para desbloquear la aplicación e interactuar con su billetera. Esto eliminará todos los códigos de acceso guardados de la billetera. Cualquier operación posterior con la billetera requerirá introducir el código de acceso. Eliminar la tarjeta guardada borra todos las billeteras guardadas y sus códigos de acceso de la app. + Requerir código de acceso + Esta opción desactiva la autenticación biométrica para acciones importantes. Se le pedirá que introduzca su código de acceso cada vez que, por ejemplo, deba firmar una transacción. Guardar código de acceso Se solicitará la autenticación biométrica en lugar del código de acceso para las interacciones con su tarjeta o anillo. Mantener la billetera en la app @@ -135,6 +143,7 @@ Cerrar Próximamente Confirme + Conectando Contacte con el soporte de Tangem Contacte con el soporte de Visa Continuar @@ -153,6 +162,7 @@ días Suprimir + Desactivar Desactivado Desconectar Listo @@ -191,6 +201,7 @@ No Ninguna dirección No aregada + Ahora no Ahora OK Abrir en el navegador @@ -206,6 +217,7 @@ Rechazar Recargar Renombrar + Requerido Guarde Guardar cambios Buscar @@ -235,6 +247,10 @@ términos y condiciones Condiciones de uso Hoy + + %d ficha + %d fichas + Transacción fallida Estado de la transacción Transacciones @@ -301,6 +317,8 @@ Detalles Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso + Recibir activos + Error de Wallet Connect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. Mis tokens @@ -358,6 +376,7 @@ Proveedor Mejor tarifa Lista de advertencias de la FCA + Proveedor en la lista de advertencia de la FCA Disponible hasta %s Disponible desde %s No disponible para este par @@ -405,6 +424,8 @@ Escanee a %s En la red %s + ¿Está seguro de que desea salir del proceso de creación del código de acceso? + Si lo haces, tendrás que empezar de nuevo. Mantente al día con las últimas funciones y noticias Esta información fue generada con IA.\nPulse aquí si encuentra algún error. Para cambiar el código de acceso coloque la tarjeta o el anillo como se muestra arriba y no lo retire hasta el fin de la operación @@ -908,9 +929,14 @@ Cantidad no válida La tarifa excede el saldo El monto total excede el saldo + Intercambiar y enviar + ¿Continuar con la conversión? Esto borrará tus datos anteriores. + Confirmar Conversión Envía cualquier token y lo convertiremos en el camino. Su destinatario obtiene exactamente lo que necesita, sin problemas. - Se enviará un destinatario + El destinatario recibirá + Al destinatario Cantidad a recibir + ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. Enviar con swap Transacción enviada Escanee la tarjeta/anillo que quiere configurar @@ -1210,8 +1236,10 @@ Consíguelo ahora con un 10 % de descuento Accede a más de 13 000 criptomonedas. Compra, vende, intercambia y realiza staking con un solo toque.\nVincula hasta tres tarjetas para hacer copias de seguridad. Descubre Tangem Wallet + Cambiar código de acceso Manténteinformado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. Notificaciones de transacciones + Establecer código de acceso Ajustes de la wallet Tangem Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera @@ -1354,6 +1382,12 @@ Desconectar todo Texto sobre desconexión de todas las dApps Desconectar todas las dApps + Intente emparejar nuevamente con una URI nueva + Dominio de dApp no válido + %s no especifica ninguna cadena de bloques, ni obligatoria ni opcional. Asegúrese de haber utilizado el URI correcto + Sin redes + Por favor, genere una nueva URI e intente conectarse nuevamente + Propuesta de conexión caducada Cambios estimados en la billetera La transacción no ha podido ser simulada. Por favor, proceda con precaución. La estimación no es compatible con %s @@ -1369,6 +1403,7 @@ No se han detectado cambios en la cartera Se han detectado riesgos potenciales o comportamiento malicioso. Conectarse o firmar transacciones puede resultar en la pérdida de fondos. Riesgo de seguridad conocido + Abra la aplicación Web3 y elija la opción WalletConnect Solicitud de Firmar de todos modos Tipo de firma diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index bbf154df4e..2ea53e27ab 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -124,6 +124,7 @@ Réclamez des récompenses Fermer Confirmez + Connexion Continuer Convertir Copier @@ -222,6 +223,10 @@ termes et conditions Conditions d\'utilisation Aujourd\'hui + + %d jeton + %d jetons + La transaction a échoué Statut de la transaction Transactions diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 5caec91961..82010eacf1 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -10,6 +10,7 @@ Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy Annulla + Connessione in corso Rimuovere Fatto Errore @@ -20,6 +21,10 @@ Invia Impossibile inviare la transazione Con successo + + %d gettone + %d gettoni + Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a598944ae0..6c32d0ad19 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,10 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アカウントをアーカイブする + アーカイブ + このアカウントをアーカイブしますが、いつでも復元できます。 + アカウント アカウントを追加 保存 アカウント名 @@ -171,6 +175,7 @@ 閉じる 近日公開 確認 + 接続中 Tangemサポートへ問い合わせる Visaサポートへ問い合わせる 続ける @@ -227,6 +232,7 @@ いいえ アドレスがありません 未追加 + 今はしない わかりました ブラウザで開く @@ -273,6 +279,9 @@ 利用規約 利用規約 今日 + + %d トークン + 取引が失敗しました 取引状況 取引 @@ -455,6 +464,7 @@ セットアップを完了するには、アクセスコードを使用してアプリへのアクセスを保護します。 そうした場合は、最初からやり直す必要があります。 アクティベーションプロセスを終了してもよろしいですか? + 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップに保存されている既存のウォレットを復元する Googleドライブのバックアップ バックアップへ移動 @@ -968,12 +978,14 @@ トークンを変更してもよろしいですか? 変更後、以前のデータはリセットされます。 トークンの変更 スワップして送信 + 変換を続行しますか? 以前のデータはリセットされます。 + 変換を確定 トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受信者に送信されます 受取金額 変換をキャンセルしてもよろしいですか? 変更後、以前のデータはリセットされます。 - キャンセルの確認 + キャンセルを確定 スワップして送信 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 @@ -1462,6 +1474,12 @@ すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する + 新しいURIで、再度ペアリングを試してください + 無効なdAppドメイン + %sはブロックチェーンを指定していません(必須でもオプションでもありません)。 \n正しいURIを使用していることを確認してください。 + ネットワークなし + 新しいURIを生成し、再度接続してください + 接続提案の期限が切れました ウォレットの変更の予測 取引をシミュレーションできませんでした。注意して続行してください。 %sでは見積もりはサポートされていません @@ -1477,6 +1495,7 @@ ウォレットの変更は検出されませんでした 潜在的なリスクまたは悪意のある行為が検出されました。接続または取引への署名は資金の損失につながる可能性があります。 既知のセキュリティリスク + Web3アプリを開き、WalletConnectオプションを選択します リクエスト元 とにかく署名する 署名タイプ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 10a53f974e..3807708210 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -121,6 +121,7 @@ Вывести награду Закрыть Подтвердить + Подключение Продолжить Копировать Скопировать адрес @@ -219,6 +220,12 @@ условия участия Условиями использования Сегодня + + %d токен + %d токена + %d токенов + %d токенов + Ошибка транзакции Статус транзакции Транзакции @@ -895,6 +902,18 @@ Недопустимая сумма Комиссия превышает остаток Отправляемая сумма превышает остаток + Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные. + Изменение токена + Обмен и отправка + Продолжить с конвертацией? Это действие удалит предыдущие данные + Подтвердить конвертацию + Отправьте любой токен, и мы конвертируем его по пути. Адресат получит именно то, что нужно — без лишних действий. + Будет получено + Получателю + Сумма к получению + Вы уверены, что хотите отменить конвертацию? Ваши предыдущие данные будут удалены. + Убрать конвертацию + Отправка с обменом Транзакция отправлена Подготовьтесь к сканированию кольца или карты, которую вы хотите настроить. Забыть кошелек diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 9c820fac97..5f7bacb6cf 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -120,6 +120,7 @@ Отримати винагороди Закрити Підтвердити + Підключення Продовжити Копіювати Скопіювати адресу @@ -218,6 +219,12 @@ умови участі Умовами використання Сьогодні + + %d токен + %d токени + %d токенів + %d токенів + Помилка транзакції Статус транзакції Транзакції diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b33588b325..256268c8fb 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -53,6 +53,7 @@ 删除 選擇代幣 關閉 + 連線中 繼續 複製 複製地址 @@ -90,6 +91,9 @@ 成功 交換 條款和條件 + + %d 代幣 + 交易 我了解 無法觸達 @@ -129,6 +133,7 @@ 更多 檢查您的網路連接或切換到其他網絡 服務條款 + WalletConnect 錯誤 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 沒有足夠的資金進行交易。請先入金 以下信息是可選的。如果不想共享,可以將其刪除 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 62f1640aa6..0b4277eab7 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -178,6 +178,7 @@ Close Coming Soon Confirm + Connecting Contact Tangem Support Contact Visa Support Continue @@ -284,6 +285,10 @@ terms and conditions Terms of Use Today + + %d token + %d tokens + Transaction failed Transaction status Transactions @@ -354,6 +359,7 @@ Sending assets in other networks will result in permanent loss. %s network Send funds using only + Wallet Connect error You have used a card or ring from another wallet. Tap the card or ring associated with this wallet Not enough funds for the transaction. Please top up your account. My tokens @@ -459,6 +465,7 @@ Scan Tangem to %s On %s network + Are you sure you want to exit the access code creation process? Backup Now To complete setup, back up your wallet and secure app access with a Access Code. Finish Now @@ -986,17 +993,17 @@ Invalid amount Fee exceeds balance Total amount exceeds balance - Are you sure you want to change the token? After changing, previous data will be reset. + Are you sure you want to change the receiving token? This will reset your previously entered data. Changing token Swap and send - Proceed with conversion? Previous data will be reset. - Confirm Convert + Proceed with conversion? This will clear your previous data. + Confirm Conversion Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive - Will be sent a recipient + To recipient Amount to receive - Are you sure you want to cancel the conversion? After changing, previous data will be reset. - Confirm cancellation + Are you sure you want to cancel the conversion? Your previous data will be cleared. + Remove Conversion Send with swap Transaction sent Prepare to scan card or ring you want to set up. @@ -1534,6 +1541,12 @@ Disconnect all Text about discnected all dApps Disconect All dApps + Try pairing again with a fresh URI + Invalid dApp domain + %s does not specify any blockchains — neither required nor optional.\nPlease ensure you used the correct URI + No networks + Please, generate a new URI and attempt connecting again + Connection proposal expired Estimated wallet changes The transaction couldn\'t be simulated. Please proceed with caution. Estimation is not supported for %s @@ -1549,6 +1562,7 @@ No wallet changes detected Potential risks or malicious behavior have been detected. Connecting or signing transactions may lead to loss of funds. Known security risk + Open Web3 app and chose WalletConnect option Request from Sign anyway Signature Type From 0617e32475456bc1cb20a310d56e219167e4e340 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 12:43:56 +0500 Subject: [PATCH 088/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 ++++ .../com/tangem/common/routing/AppRoute.kt | 5 ++ .../hotwallet/UpdateAccessCodeComponent.kt | 10 ++++ .../AccessCodeComponent.kt | 4 +- .../AccessCodeModel.kt | 4 +- .../hotwallet/accesscode/Constants.kt | 3 + .../di/AccessCodeModule.kt | 4 +- .../entity/AccessCodeUM.kt | 4 +- .../ui/AccessCode.kt | 4 +- .../HotAccessCodeRequestModel.kt | 2 +- .../entry/AddExistingWalletModel.kt | 2 +- .../routing/AddExistingWalletChildFactory.kt | 2 +- .../hotwallet/setaccesscode/Constants.kt | 3 - .../DefaultUpdateAccessCodeComponent.kt | 60 +++++++++++++++++++ .../UpdateAccessCodeContent.kt | 47 +++++++++++++++ .../updateaccesscode/UpdateAccessCodeModel.kt | 45 ++++++++++++++ .../di/UpdateAccessCodeModule.kt | 27 +++++++++ .../routing/UpdateAccessCodeChildFactory.kt | 38 ++++++++++++ .../routing/UpdateAccessCodeRoute.kt | 14 +++++ .../entry/WalletActivationModel.kt | 2 +- .../routing/WalletActivationChildFactory.kt | 2 +- .../model/WalletSettingsModel.kt | 2 +- 22 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode}/AccessCodeComponent.kt (93%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode}/AccessCodeModel.kt (96%) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode}/di/AccessCodeModule.kt (78%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode}/entity/AccessCodeUM.kt (69%) rename features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/{setaccesscode => accesscode}/ui/AccessCode.kt (97%) delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index ba64c1cfc6..8c05bbd7b2 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -17,6 +17,7 @@ import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.UpdateAccessCodeComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -95,6 +96,7 @@ internal class ChildFactory @Inject constructor( private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val walletActivationComponentFactory: WalletActivationComponent.Factory, + private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -479,6 +481,15 @@ internal class ChildFactory @Inject constructor( componentFactory = walletActivationComponentFactory, ) } + is AppRoute.UpdateAccessCode -> { + createComponentChild( + context = context, + params = UpdateAccessCodeComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = updateAccessCodeComponentFactory, + ) + } is AppRoute.SendEntryPoint -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 3b71b7518e..304f1cb956 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -308,6 +308,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") + @Serializable + data class UpdateAccessCode( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}") + @Serializable data class SendEntryPoint( val userWalletId: UserWalletId, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt new file mode 100644 index 0000000000..b2416c8d0a --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpdateAccessCodeComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface UpdateAccessCodeComponent : ComposableContentComponent { + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt similarity index 93% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt index 6cd119cdfb..5a044f85ef 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode +package com.tangem.features.hotwallet.accesscode import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,7 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect -import com.tangem.features.hotwallet.setaccesscode.ui.AccessCode +import com.tangem.features.hotwallet.accesscode.ui.AccessCode import com.tangem.domain.models.wallet.UserWalletId import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt similarity index 96% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index e387f3e404..a7e9eec930 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode +package com.tangem.features.hotwallet.accesscode import androidx.compose.runtime.Stable import arrow.core.getOrElse @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.hotwallet.setaccesscode.entity.AccessCodeUM +import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth import com.tangem.hot.sdk.model.UnlockHotWallet diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt new file mode 100644 index 0000000000..ca5ecaa367 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/Constants.kt @@ -0,0 +1,3 @@ +package com.tangem.features.hotwallet.accesscode + +const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt similarity index 78% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt index c6fb7f93cc..57353dd676 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/di/AccessCodeModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/di/AccessCodeModule.kt @@ -1,7 +1,7 @@ -package com.tangem.features.hotwallet.setaccesscode.di +package com.tangem.features.hotwallet.accesscode.di import com.tangem.core.decompose.model.Model -import com.tangem.features.hotwallet.setaccesscode.AccessCodeModel +import com.tangem.features.hotwallet.accesscode.AccessCodeModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt similarity index 69% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt index 9141f7f4c5..11eccc1463 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/AccessCodeUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/entity/AccessCodeUM.kt @@ -1,6 +1,6 @@ -package com.tangem.features.hotwallet.setaccesscode.entity +package com.tangem.features.hotwallet.accesscode.entity -import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH internal data class AccessCodeUM( val accessCode: String, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt similarity index 97% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 923b475395..987c94c953 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -1,4 +1,4 @@ -package com.tangem.features.hotwallet.setaccesscode.ui +package com.tangem.features.hotwallet.accesscode.ui import android.content.res.Configuration import androidx.compose.foundation.background @@ -16,7 +16,7 @@ import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.hotwallet.setaccesscode.entity.AccessCodeUM +import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM @Suppress("LongParameterList", "LongMethod") @Composable diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 05e2ef611b..305bd305bb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -3,7 +3,7 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.wallets.hot.HotWalletPasswordRequester -import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH +import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt index ca924d1eb8..328e74cc8c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/AddExistingWalletModel.kt @@ -11,7 +11,7 @@ import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExisting import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent -import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt index 8a25cc1176..1e0fd88f21 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/entry/routing/AddExistingWalletChildFactory.kt @@ -6,7 +6,7 @@ import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletMo import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent -import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt deleted file mode 100644 index 8494ba6e8b..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/Constants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.features.hotwallet.setaccesscode - -const val ACCESS_CODE_LENGTH = 6 \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt new file mode 100644 index 0000000000..127f506d0c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/DefaultUpdateAccessCodeComponent.kt @@ -0,0 +1,60 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeChildFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultUpdateAccessCodeComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: UpdateAccessCodeComponent.Params, + private val childFactory: UpdateAccessCodeChildFactory, +) : UpdateAccessCodeComponent, AppComponentContext by appComponentContext { + + private val model: UpdateAccessCodeModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "hotWalletAccessCodeInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + childFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + + BackHandler(onBack = model::onChildBack) + + SetAccessCodeContent( + onBackClick = model::onChildBack, + stackState = stackState, + ) + } + + @AssistedFactory + interface Factory : UpdateAccessCodeComponent.Factory { + override fun create( + context: AppComponentContext, + params: UpdateAccessCodeComponent.Params, + ): DefaultUpdateAccessCodeComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt new file mode 100644 index 0000000000..e8f3c4a40d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeContent.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeRoute + +@Composable +internal fun SetAccessCodeContent( + onBackClick: () -> Unit, + stackState: ChildStack, +) { + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier, + title = stringResourceSafe(R.string.access_code_navtitle), + startButton = TopAppBarButtonUM.Back(onBackClick), + ) + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt new file mode 100644 index 0000000000..19f8947793 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/UpdateAccessCodeModel.kt @@ -0,0 +1,45 @@ +package com.tangem.features.hotwallet.updateaccesscode + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.routing.UpdateAccessCodeRoute +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class UpdateAccessCodeModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model(), AccessCodeComponent.ModelCallbacks { + + private val params = paramsContainer.require() + + val stackNavigation = StackNavigation() + val startRoute: UpdateAccessCodeRoute = UpdateAccessCodeRoute.SetAccessCode(params.userWalletId) + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onChildBack() { + when (currentRoute.value) { + is UpdateAccessCodeRoute.SetAccessCode -> router.pop() + is UpdateAccessCodeRoute.ConfirmAccessCode -> stackNavigation.pop() + } + } + + override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) { + stackNavigation.push(UpdateAccessCodeRoute.ConfirmAccessCode(userWalletId, accessCode)) + } + + override fun onAccessCodeConfirmed(userWalletId: UserWalletId) { + router.pop() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt new file mode 100644 index 0000000000..478deec433 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/di/UpdateAccessCodeModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.hotwallet.updateaccesscode.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.DefaultUpdateAccessCodeComponent +import com.tangem.features.hotwallet.updateaccesscode.UpdateAccessCodeModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface UpdateAccessCodeModule { + + @Binds + fun bindUpdateAccessCodeComponentFactory( + impl: DefaultUpdateAccessCodeComponent.Factory, + ): UpdateAccessCodeComponent.Factory + + @Binds + @IntoMap + @ClassKey(UpdateAccessCodeModel::class) + fun bindUpdateAccessCodeModel(model: UpdateAccessCodeModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt new file mode 100644 index 0000000000..354156a4c0 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeChildFactory.kt @@ -0,0 +1,38 @@ +package com.tangem.features.hotwallet.updateaccesscode.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.updateaccesscode.UpdateAccessCodeModel +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent +import javax.inject.Inject + +internal class UpdateAccessCodeChildFactory @Inject constructor( + private val accessCodeComponentFactory: AccessCodeComponent.Factory, +) { + + fun createChild( + route: UpdateAccessCodeRoute, + childContext: AppComponentContext, + model: UpdateAccessCodeModel, + ): ComposableContentComponent { + return when (route) { + is UpdateAccessCodeRoute.SetAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = false, + userWalletId = route.userWalletId, + callbacks = model, + ), + ) + is UpdateAccessCodeRoute.ConfirmAccessCode -> accessCodeComponentFactory.create( + context = childContext, + params = AccessCodeComponent.Params( + isConfirmMode = true, + accessCodeToConfirm = route.accessCode, + userWalletId = route.userWalletId, + callbacks = model, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt new file mode 100644 index 0000000000..a414995364 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/updateaccesscode/routing/UpdateAccessCodeRoute.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet.updateaccesscode.routing + +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +internal sealed class UpdateAccessCodeRoute : Route { + + @Serializable + data class SetAccessCode(val userWalletId: UserWalletId) : UpdateAccessCodeRoute() + + @Serializable + data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : UpdateAccessCodeRoute() +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt index 3d03a96a04..f163ba6e18 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/WalletActivationModel.kt @@ -14,7 +14,7 @@ import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckCompone import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent -import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.routing.WalletActivationRoute import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt index 2b93835929..59762e8fe3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletactivation/entry/routing/WalletActivationChildFactory.kt @@ -6,7 +6,7 @@ import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckCompone import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent -import com.tangem.features.hotwallet.setaccesscode.AccessCodeComponent +import com.tangem.features.hotwallet.accesscode.AccessCodeComponent import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel import com.tangem.features.pushnotifications.api.PushNotificationsComponent diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 061c68bb82..22dd035507 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -330,6 +330,6 @@ internal class WalletSettingsModel @Inject constructor( } private fun onAccessCodeClick() { - // TODO [REDACTED_TASK_KEY] + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) } } \ No newline at end of file From 229a13d0ea0d26fbac03e985da293eeb8023cd34 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 08:55:41 +0000 Subject: [PATCH 089/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..b1088cbc11 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "develop-511" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-454" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From 947c5e44e646985b85128d93ee69d4f28a607e69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 17:53:32 +0400 Subject: [PATCH 090/165] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 6 +- .../DefaultAccountsCRUDRepository.kt | 16 +- .../domain/account/models/ArchivedAccount.kt | 29 +++ .../repository/AccountsCRUDRepository.kt | 8 + .../usecase/RecoverCryptoPortfolioUseCase.kt | 118 +++++++++- .../RecoverCryptoPortfolioUseCaseTest.kt | 208 ++++++++++++++++++ 6 files changed, 374 insertions(+), 11 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 41d100f879..17e09eb709 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -39,7 +39,9 @@ internal object AccountDomainModule { @Provides @Singleton - fun provideRecoverCryptoPortfolioUseCase(): RecoverCryptoPortfolioUseCase { - return RecoverCryptoPortfolioUseCase() + fun provideRecoverCryptoPortfolioUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): RecoverCryptoPortfolioUseCase { + return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 76d86bd0fe..723da17196 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -3,12 +3,13 @@ package com.tangem.data.account.repository import arrow.core.Option import arrow.core.Option.Companion.catch import arrow.core.none +import arrow.core.raise.option import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace @@ -35,6 +36,17 @@ internal class DefaultAccountsCRUDRepository( ?: return none() } + override suspend fun getArchivedAccount(accountId: AccountId): Option = option { + ArchivedAccount( + accountId = accountId, + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = DerivationIndex(value = 1000).getOrNull()!!, + tokensCount = 2, + networksCount = 1, + ) + } + override suspend fun saveAccounts(accountList: AccountList) { runtimeStore.update(emptyList()) { it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt b/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt new file mode 100644 index 0000000000..1c017a4f88 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/models/ArchivedAccount.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.account.models + +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import kotlinx.serialization.Serializable + +/** + * Represents an archived crypto portfolio account + * + * @property accountId the unique identifier of the archived account + * @property name the name of the archived account + * @property icon the icon representing the archived account + * @property derivationIndex the derivation index for the archived account + * @property tokensCount the number of tokens in the archived account + * @property networksCount the number of networks associated with the archived account + * +[REDACTED_AUTHOR] + */ +@Serializable +data class ArchivedAccount( + val accountId: AccountId, + val name: AccountName, + val icon: CryptoPortfolioIcon, + val derivationIndex: DerivationIndex, + val tokensCount: Int, + val networksCount: Int, +) \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index 70b8543734..a32796f0a1 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.account.repository import arrow.core.Option import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet @@ -30,6 +31,13 @@ interface AccountsCRUDRepository { */ suspend fun getAccount(accountId: AccountId): Option + /** + * Retrieves a archived account by its unique identifier + * + * @param accountId the unique identifier of the account + */ + suspend fun getArchivedAccount(accountId: AccountId): Option + /** * Saves a list of accounts to the repository * diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index 2d27ad3a32..9679a036b6 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -1,25 +1,129 @@ package com.tangem.domain.account.usecase import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId /** + * Use case for recovering a crypto portfolio account from archived accounts + * + * @property crudRepository repository for performing CRUD operations on accounts + * [REDACTED_AUTHOR] */ -class RecoverCryptoPortfolioUseCase { +class RecoverCryptoPortfolioUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + /** + * Recovers a crypto portfolio account by moving it from archived accounts to active accounts + * + * @param accountId the unique identifier of the account to recover + */ suspend operator fun invoke(accountId: AccountId): Either = either { - raise(Error.DataOperationFailed) + val accountList = getAccountList(userWalletId = accountId.userWalletId) + val archivedAccount = getArchivedAccount(accountId = accountId) - // TODO: [REDACTED_JIRA] - // Remove the account from the list of archived accounts - // Add the account to the list of active accounts - // Save to backend + val recoveredAccount = archivedAccount.recover() + + val updatedAccountList = (accountList + recoveredAccount) + .getOrElse { raise(Error.CriticalTechError.AccountListRequirementsNotMet(cause = it)) } + + saveAccounts(updatedAccountList) + + recoveredAccount } + private suspend fun Raise.getAccountList(userWalletId: UserWalletId): AccountList { + return catch( + block = { crudRepository.getAccounts(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { raise(Error.CriticalTechError.AccountsNotCreated(userWalletId = userWalletId)) } + } + + private suspend fun Raise.getArchivedAccount(accountId: AccountId): ArchivedAccount { + return catch( + block = { crudRepository.getArchivedAccount(accountId = accountId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + .getOrElse { + raise(Error.CriticalTechError.AccountNotFound(accountId = accountId)) + } + } + + private fun ArchivedAccount.recover(): Account.CryptoPortfolio { + return Account.CryptoPortfolio( + accountId = this.accountId, + accountName = this.name, + accountIcon = this.icon, + derivationIndex = this.derivationIndex, + isArchived = false, + cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( + currencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ) + } + + private suspend fun Raise.saveAccounts(accountList: AccountList) { + catch( + block = { crudRepository.saveAccounts(accountList) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur during the add operation + */ sealed interface Error { - data object DataOperationFailed : Error + + val tag: String + get() = this::class.simpleName ?: "RecoverCryptoPortfolioUseCase.Error" + + /** + * Critical technical errors that can occur during the recovery operation + */ + sealed interface CriticalTechError : Error { + + /** + + * + * @property userWalletId the unique identifier of the user wallet + */ + data class AccountsNotCreated(val userWalletId: UserWalletId) : CriticalTechError { + override fun toString(): String = "$tag: Accounts for $userWalletId are not created" + } + + /** Error indicating that the account with [accountId] was not found */ + data class AccountNotFound(val accountId: AccountId) : CriticalTechError { + override fun toString(): String = "$tag: Account with ID $accountId not found" + } + + /** + * Error indicating that the account list requirements were not met. + * + * @property cause the underlying cause of the error + */ + data class AccountListRequirementsNotMet(val cause: AccountList.Error) : Error { + override fun toString(): String = "$tag: Account list requirements not met: $cause" + } + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } } } \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt new file mode 100644 index 0000000000..316e789754 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -0,0 +1,208 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.left +import arrow.core.right +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error +import com.tangem.domain.account.utils.createAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RecoverCryptoPortfolioUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = RecoverCryptoPortfolioUseCase(crudRepository) + private val userWallet = mockk() + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository, userWallet) + every { userWallet.walletId } returns userWalletId + } + + @Test + fun `invoke should recover archived crypto portfolio account`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val archivedAccount = ArchivedAccount( + accountId = account.accountId, + name = account.name, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val recoveredAccount = account.copy(isArchived = false) + val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = recoveredAccount.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + @Test + fun `invoke should return error if getAccounts returns None`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + + coEvery { crudRepository.getAccounts(userWalletId) } returns None + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.CriticalTechError.AccountsNotCreated(userWalletId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { + crudRepository.getArchivedAccount(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getAccounts throws exception`() = runTest { + // Arrange + val accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = DerivationIndex.Main, + ) + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } throws exception + + // Act + val actual = useCase(accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { crudRepository.getAccounts(userWalletId) } + coVerify(inverse = true) { + crudRepository.getArchivedAccount(any()) + crudRepository.saveAccounts(any()) + } + } + + @Test + fun `invoke should return error if getArchivedAccount throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val exception = IllegalStateException("Test error") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } throws exception + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if getArchivedAccount returns null`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns None + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.CriticalTechError.AccountNotFound(account.accountId).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + } + coVerify(inverse = true) { crudRepository.saveAccounts(any()) } + } + + @Test + fun `invoke should return error if saveAccounts throws exception`() = runTest { + // Arrange + val account = createAccount(userWalletId) + val accountList = AccountList.empty(userWallet) + val archivedAccount = ArchivedAccount( + accountId = account.accountId, + name = account.name, + icon = account.icon, + derivationIndex = account.derivationIndex, + tokensCount = 1, + networksCount = 1, + ) + + val recoveredAccount = account.copy(isArchived = false) + val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val exception = IllegalStateException("Save failed") + + coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() + coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() + coEvery { crudRepository.saveAccounts(updatedAccountList) } throws exception + + // Act + val actual = useCase(account.accountId) + + // Assert + val expected = Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getAccounts(userWalletId) + crudRepository.getArchivedAccount(account.accountId) + crudRepository.saveAccounts(updatedAccountList) + } + } + + private companion object { + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file From ee097ad9f0a022543801c6b39ef2092c3ec1de89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 17:22:08 +0500 Subject: [PATCH 091/165] Updated on 2026-08-14 --- .../converters/AmountStateConverter.kt | 8 +- .../field/AmountBoundaryUpdateTransformer.kt | 9 +- .../amountScreen/ui/AmountFieldContainer.kt | 4 +- .../ui/amountScreen/ui/AmountFieldV2.kt | 11 +- core/res/src/main/res/values-de/strings.xml | 8 + core/res/src/main/res/values-es/strings.xml | 3 +- core/res/src/main/res/values-fr/strings.xml | 8 + core/res/src/main/res/values-it/strings.xml | 9 + core/res/src/main/res/values-ja/strings.xml | 13 +- core/res/src/main/res/values-ru/strings.xml | 12 +- .../src/main/res/values-uk-rUA/strings.xml | 49 ++++ .../src/main/res/values-zh-rTW/strings.xml | 8 + core/res/src/main/res/values/strings.xml | 8 +- .../destination/ui/DestinationBlock.kt | 2 +- .../converter/SwapAmountFieldConverter.kt | 21 +- .../impl/amount/ui/SwapAmountBlockContent.kt | 5 +- .../v2/impl/amount/ui/SwapAmountContent.kt | 216 ++++++++---------- 17 files changed, 247 insertions(+), 147 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 937f94852b..ac0ebecf2f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -18,6 +19,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf @@ -120,7 +122,11 @@ class AmountStateConverterV2( return AmountState.Data( title = value.title, availableBalance = if (isRedesignEnabled) { - resourceReference(R.string.common_balance, wrappedList(crypto)) + combinedReference( + stringReference(crypto), + stringReference(" $DOT "), + stringReference(fiat), + ) } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) }, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 8761c5abb0..89c1074a49 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -3,13 +3,16 @@ package com.tangem.common.ui.amountScreen.converters.field import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.transformer.Transformer /** @@ -34,7 +37,11 @@ class AmountBoundaryUpdateTransformer( val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } val availableBalance = if (isRedesignEnabled) { - resourceReference(R.string.common_balance, wrappedList(crypto)) + combinedReference( + stringReference(crypto), + stringReference(" $DOT "), + stringReference(fiat), + ) } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index a1ba4366f4..9c31705709 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -113,7 +113,7 @@ internal fun LazyListScope.amountFieldV2( } else { Text( text = amountState.title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) } @@ -164,7 +164,7 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi modifier = Modifier .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index 184f7ed46e..96635bbc45 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -127,7 +127,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - modifier = Modifier .fillMaxWidth() .animateContentSize() - .padding(top = 8.dp), + .padding(top = 4.dp), ) { if (amountUM is AmountState.Empty) { TextShimmer( @@ -141,6 +141,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - AmountFieldCurrencyInfo( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + ) AmountFieldError( isError = amountUM.amountTextField.isError, @@ -148,7 +149,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - error = amountUM.amountTextField.error, modifier = Modifier .align(BottomCenter) - .padding(top = 20.dp), + .padding(top = 24.dp), ) } } @@ -161,12 +162,13 @@ private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurre horizontalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .align(TopCenter) - .padding(bottom = 20.dp) + .padding(bottom = 16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) }, - ), + ) + .padding(4.dp), ) { val iconRotateState by animateFloatAsState( targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE, @@ -324,6 +326,7 @@ private class AmountFieldV2PreviewProvider : PreviewParameterProviderDetails Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen + Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt. + WalletConnect-Fehler Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist. Nicht genug Geld für die Transaktion. Bitte lade dein Konto auf. Meine Token @@ -1431,6 +1433,12 @@ Alle trennen Text über die Trennung aller dApps Alle dApps trennen + Versuchen Sie erneut, mit einer neuen URI zu koppeln + Ungültige dApp-Domain + %s gibt keine Blockchains an — weder erforderlich noch optional.\nBitte stellen Sie sicher, dass Sie die richtige URI verwendet haben + Keine Netzwerke + Bitte generieren Sie eine neue URI und versuchen Sie erneut, eine Verbindung herzustellen + Verbindungsvorschlag abgelaufen Geschätzte Wallet-Änderungen Die Transaktion konnte nicht simuliert werden. Bitte sei vorsichtig. Böswillige/ gefährliche Transaktion diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index f134baf614..8d38231daf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -318,7 +318,8 @@ Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Recibir activos - Error de Wallet Connect + Hola equipo de soporte, he encontrado un error con el código: %s + Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. Mis tokens diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 2ea53e27ab..00b0250a97 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -293,6 +293,8 @@ Détails Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation + Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s + Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille Pas assez de fonds pour la transaction. Veuillez recharger votre compte. Mes jetons @@ -1332,6 +1334,12 @@ Déconnecter tout Texte sur la déconnexion de toutes les dApps Déconnecter toutes les dApps + Essayez de jumeler à nouveau avec un nouvel URI + Domaine dApp invalide + %s ne spécifie aucune blockchain — ni requise ni optionnelle.\nVeuillez vous assurer que vous avez utilisé l’URI correct + Aucun réseau + Veuillez générer un nouvel URI et essayer de vous connecter à nouveau + La proposition de connexion a expiré Modifications estimées du portefeuille La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence. Transaction malveillante diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 82010eacf1..a585f2e30a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -10,6 +10,7 @@ Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy Annulla + Scegli portafoglio Connessione in corso Rimuovere Fatto @@ -39,6 +40,8 @@ Firmato Requisiti Termini del servizio + Ciao team di supporto, ho riscontrato un errore con il codice: %s + Errore WalletConnect Not enough funds for the transaction. Please top up your account. An error occurred Notifica richiesta @@ -71,6 +74,12 @@ Nessuna connessione a Internet Tangem Ok, ho capito! + Prova a eseguire nuovamente l’associazione con un nuovo URI + Dominio dApp non valido + %s non specifica alcuna blockchain — né obbligatoria né opzionale.\nAssicurati di aver utilizzato l’URI corretto + Nessuna rete + Genera un nuovo URI e prova a connetterti di nuovo + La proposta di connessione è scaduta No, invia l\'intero importo Riduci di %s XTZ Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6c32d0ad19..ff32b8bf8b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -352,6 +352,8 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + サポートチームの皆様、コード %s のエラーが発生しました。 + WalletConnect エラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 取引に必要な資金が不足しています。アカウントに入金してください。 マイトークン @@ -457,6 +459,7 @@ Tangemをスキャン %sへ %sネットワーク + アクセスコードの作成プロセスを終了してもよろしいですか? 今すぐバックアップ セットアップを完了するには、ウォレットをバックアップし、アクセスコードを使用してアプリへのアクセスを保護します。 今すぐ実施 @@ -975,17 +978,17 @@ 無効な金額 手数料が残高を超えています 合計金額が残高を超えています - トークンを変更してもよろしいですか? 変更後、以前のデータはリセットされます。 + 受信トークンを変更してもよろしいですか? 変更すると、以前入力したデータがリセットされます。 トークンの変更 スワップして送信 - 変換を続行しますか? 以前のデータはリセットされます。 + 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります - 受信者に送信されます + 受取人へ 受取金額 - 変換をキャンセルしてもよろしいですか? 変更後、以前のデータはリセットされます。 - キャンセルを確定 + 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 + 変換を削除 スワップして送信 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 3807708210..5b238275be 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -116,7 +116,7 @@ Выберите действие Выберите сеть Выберите токен - Выберите кошелек + Выберите кошелёк Получить Вывести награду Закрыть @@ -291,6 +291,8 @@ Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования + Привет, команда поддержки, у меня возникла ошибка с кодом: %s + Ошибка WalletConnect Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Мои токены @@ -1304,7 +1306,7 @@ Требуется трастлайн Вредоносный домен Неизвестный домен - Подключиться всё равно + Всё равно подключиться Ошибка тайм-аута. Пожалуйста, попробуйте позже. Не удалось подключиться через Wallet Connect Этот домен не может быть верифицирован. Внимательно проверьте запрос перед одобрением. @@ -1348,6 +1350,12 @@ dApp отключен Отключить все Отключить все dApp + Попробуйте соединиться снова, используя новый URI + Недействительный домен dApp + %s не указывает никаких блокчейнов — ни обязательных, ни опциональных.\nПожалуйста, убедитесь, что вы использовали правильный URI + Нет сетей + Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова + Предложение подключения истекло Предварительные изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 5f7bacb6cf..79d1d3c092 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -192,6 +192,7 @@ Відхилити Оновити Перейменувати + Обов\'язково Зберегти Зберегти зміни Пошук @@ -289,6 +290,8 @@ Деталі Перевірте підключення до інтернету або змініть мережу Умови використання + Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s + Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. Мої токени @@ -1289,18 +1292,64 @@ Відкрити Trustline Щоб отримати цей токен, потрібно увімкнути Trustline. Мережа вимагає резерв %1$s %2$s. Відкрийте Trustline + Невідомий домен + Все одно підключитися + Помилка тайм-ауту. Будь ласка, спробуйте пізніше. + Не вдалося зʼєднатися через Wallet Connect + Цей домен не може бути підтверджений. Уважно перевірте запит перед схваленням. Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. + Сеанс Wallet Connect було завершено + Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. + Ми зіткнулися з невідомою помилкою + Tangem наразі не підтримує необхідну мережу для %s. + Непідтримувані мережі + Tangem підтримує мережу, необхідну для %s + Верифікований домен Обрана не вірна картка або кільце + Схоже, виникла проблема + Усі dApps відключені Адреса Підключення + Мережа + Мережі + Гаманець + Підключений додаток Підключені мережі + Підключений до %1$s Переглянути баланс гаманця та активність + Підписати транзакцію без вашої участі + Запит схвалення на транзакцію + Не має можливості + Хотіли б Запит на підключення + З\'єднання Вміст Копіювати дані + dApp відключено + Розʼєднати все + Відключити всі dApps + Відключити всі dApps + Спробуйте ще раз з новим URI + Недійсний домен dApp + %s не вказує жодних блокчейнів — ні обов\'язкових, ні необов\'язкових. \n Переконайтеся, що ви використали правильний URI + Немає мереж + Будь ласка, згенеруйте новий URI та спробуйте ще раз + Термін для з’єднання минув + Поповніть баланс, щоб покрити комісію мережі + Недостатньо %1$s + Додайте %s мережі до вашого портфелю для цього гаманця + В гаманці не додані необхідні мережі + Нове з\'єднання + Підключайте свій гаманець до різних dApps + Немає підключень Виявлено потенційні ризики або шкідливу активність. Підключення чи підпис транзакцій можуть призвести до втрати коштів. + Відомий ризик безпеки + Відкрийте програму Web3 та виберіть опцію WalletConnect Запит від Тип підпису + Для підключення dApp потрібна принаймні одна мережа + Вкажіть вибрані мережі + Успішно підписано До Запит транзакції Запит транзакції diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 256268c8fb..713520796a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -52,6 +52,7 @@ 您尚未授予相機訪問權限,請更改您的隱私設置 删除 選擇代幣 + 選擇錢包 關閉 連線中 繼續 @@ -133,6 +134,7 @@ 更多 檢查您的網路連接或切換到其他網絡 服務條款 + 您好,支援團隊,我遇到了一個錯誤,代碼為:%s WalletConnect 錯誤 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 沒有足夠的資金進行交易。請先入金 @@ -377,6 +379,12 @@ 認證檢查失敗 網路無法使用 Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 + 請使用新的 URI 再次嘗試配對 + 無效的 dApp 網域 + %s 未指定任何區塊鏈——無論是必須的還是可選的。\n請確保您使用了正確的 URI + 沒有網路 + 請生成新的 URI,然後再次嘗試連線 + 連線提案已過期 捨棄 您有一個備份中斷了,您想繼續嗎? 是的,恢復 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0b4277eab7..4089de8ca8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,6 +12,8 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + Recover + Archived Archive account Archive You are archiving this account, but you can always get it back. @@ -359,7 +361,8 @@ Sending assets in other networks will result in permanent loss. %s network Send funds using only - Wallet Connect error + Hi support team, I\'ve encountered an error with code: %s + WalletConnect error You have used a card or ring from another wallet. Tap the card or ring associated with this wallet Not enough funds for the transaction. Please top up your account. My tokens @@ -1002,6 +1005,7 @@ Recipient will receive To recipient Amount to receive + Recipient get %s Are you sure you want to cancel the conversion? Your previous data will be cleared. Remove Conversion Send with swap @@ -1569,7 +1573,7 @@ At least one network is required for dApp connection Specify selected networks Successfully signed - Wallet connect + WalletConnect To Transaction request Transaction request diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index cea354ba6e..f8cf59bdb0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -134,7 +134,7 @@ private fun AddressWithMemoBlock( Column(modifier = Modifier.weight(1f)) { Text( text = address.value, - style = TangemTheme.typography.body2, + style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, ) val blockchainAddress = address.briefBlockchainAddress diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index f29f727166..fd83068f8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -16,6 +17,7 @@ import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.utils.StringsSigns.DOT internal class SwapAmountFieldConverter( private val swapDirection: SwapDirection, @@ -60,15 +62,26 @@ internal class SwapAmountFieldConverter( } private fun getSubtitle(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> resourceReference( - R.string.common_balance, - wrappedList( + selectedType.isEnteringField() -> combinedReference( + stringReference( cryptoCurrencyStatus.value.amount.format { crypto(cryptoCurrency = cryptoCurrencyStatus.currency) }, ), + stringReference(value = " $DOT "), + stringReference( + cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), ).orMaskWithStars(isBalanceHidden) - selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_amount_text) + selectedType.isViewingField() -> resourceReference( + R.string.send_with_swap_recipient_get_amount, + + ) else -> TextReference.Companion.EMPTY } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index f68cfcadc4..b788f33190 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -154,10 +154,7 @@ private fun SwapAmountDivider(modifier: Modifier = Modifier) { style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .background(TangemTheme.colors.stroke.primary, RoundedCornerShape(32.dp)) - .padding(1.dp) - .background(TangemTheme.colors.text.primary2, RoundedCornerShape(32.dp)) // workaround - .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f), RoundedCornerShape(32.dp)) + .background(TangemTheme.colors.button.secondary, RoundedCornerShape(32.dp)) .padding(horizontal = 11.dp, vertical = 5.dp) .align(Alignment.Center), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 199921a753..41957bf386 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -3,8 +3,6 @@ package com.tangem.features.swap.v2.impl.amount.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -26,7 +24,7 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountFieldV2 -import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH2 import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText @@ -38,7 +36,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType @@ -54,13 +51,17 @@ internal fun SwapAmountContent( clickIntents: SwapAmountClickIntents, modifier: Modifier = Modifier, ) { + val swapAmountContent = amountUM as? SwapAmountUM.Content + val isFixedRate = swapAmountContent?.swapRateType == ExpressRateType.Fixed ConstraintLayout( modifier = modifier, ) { val (amountFromRef, amountToRef, middleButtonRef) = createRefs() SwapAmountBlock( - amountUM = amountUM, amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + selectedQuote = swapAmountContent?.selectedQuote, + isFixedRate = isFixedRate, clickIntents = clickIntents, modifier = Modifier.constrainAs(amountFromRef) { top.linkTo(parent.top) @@ -69,8 +70,10 @@ internal fun SwapAmountContent( }, ) SwapAmountBlock( - amountUM = amountUM, amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + selectedQuote = swapAmountContent?.selectedQuote, + isFixedRate = isFixedRate, clickIntents = clickIntents, modifier = Modifier.constrainAs(amountToRef) { top.linkTo(amountFromRef.bottom, 8.dp) @@ -100,7 +103,7 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M modifier = modifier .heightIn(max = 28.dp) .clip(RoundedCornerShape(32.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable(onClick = onClick) .padding(vertical = 6.dp, horizontal = 12.dp), ) { @@ -109,10 +112,6 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) - VerticalDivider( - thickness = 1.dp, - color = TangemTheme.colors.icon.inactive, - ) Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_close_24), @@ -126,11 +125,14 @@ private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = M @Composable private fun SwapAmountBlock( - amountUM: SwapAmountUM, amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + selectedAmountType: SwapAmountType, + isFixedRate: Boolean, clickIntents: SwapAmountClickIntents, modifier: Modifier = Modifier, ) { + val isSelectedAmountType = selectedAmountType == amountFieldUM.amountType Column( modifier = modifier .padding(horizontal = 16.dp) @@ -138,7 +140,7 @@ private fun SwapAmountBlock( .fillMaxWidth() .background(TangemTheme.colors.background.action), ) { - AnimatedVisibility(amountUM.selectedAmountType == amountFieldUM.amountType) { + AnimatedVisibility(isSelectedAmountType) { Box { SwapAmountEditBlock( amountFieldUM = amountFieldUM, @@ -157,8 +159,10 @@ private fun SwapAmountBlock( } } SwapAmountInfo( - amountUM = amountUM, amountFieldUM = amountFieldUM, + selectedQuote = selectedQuote, + isSelectedAmountType = isSelectedAmountType, + isFixedRate = isFixedRate, onExpandEditField = clickIntents::onExpandEditField, onSelectTokenClick = clickIntents::onSelectTokenClick, onMaxAmountClick = clickIntents::onMaxValueClick, @@ -187,7 +191,7 @@ private fun SwapAmountEditBlock( } else { Text( text = (amountFieldUM.amountField as AmountState.Data).title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) } @@ -201,10 +205,13 @@ private fun SwapAmountEditBlock( } } +@Suppress("LongParameterList") @Composable private fun SwapAmountInfo( - amountUM: SwapAmountUM, amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + isSelectedAmountType: Boolean, + isFixedRate: Boolean, onExpandEditField: (SwapAmountType) -> Unit, onMaxAmountClick: () -> Unit, onSelectTokenClick: () -> Unit, @@ -220,7 +227,7 @@ private fun SwapAmountInfo( indication = ripple(), enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, onClick = { - if ((amountUM as? SwapAmountUM.Content)?.swapRateType == ExpressRateType.Fixed) { + if (isFixedRate) { onExpandEditField(amountFieldUM.amountType) } else { onSelectTokenClick() @@ -237,107 +244,85 @@ private fun SwapAmountInfo( bottom = 16.dp, ), ) - SwapAmountInfoMain(amountFieldUM = amountFieldUM) + SwapAmountInfoMain( + amountFieldUM = amountFieldUM, + selectedQuote = selectedQuote, + isSelectedAmountType = isSelectedAmountType, + ) SpacerWMax() AnimatedContent( - amountFieldUM, - ) { wrappedFieldAmountUM -> - if (amountUM is SwapAmountUM.Content) { - SwapAmountInfoExtra( - amountUM = amountUM, - amountFieldUM = wrappedFieldAmountUM, - onMaxAmountClick = onMaxAmountClick, + targetState = isSelectedAmountType, + ) { isSelected -> + if (isSelected) { + AmountMaxButton(onMaxAmountClick) + } else { + SwapAmountInfoQuote( + quoteUM = selectedQuote, + isFixedRate = isFixedRate, onSelectTokenClick = onSelectTokenClick, ) - } else { - RectangleShimmer() } } } } @Composable -private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modifier = Modifier) { - AnimatedContent( - targetState = amountFieldUM is SwapAmountFieldUM.Content, +private fun SwapAmountInfoMain( + amountFieldUM: SwapAmountFieldUM, + selectedQuote: SwapQuoteUM?, + isSelectedAmountType: Boolean, + modifier: Modifier = Modifier, +) { + Column( modifier = modifier, - ) { isContent -> - if (isContent && amountFieldUM is SwapAmountFieldUM.Content) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { + ) { + AnimatedContent(amountFieldUM is SwapAmountFieldUM.Content) { isContent -> + if (isContent && amountFieldUM is SwapAmountFieldUM.Content) { Text( text = amountFieldUM.title.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, maxLines = 1, ) + } else { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + } + } + AnimatedContent(isSelectedAmountType) { isSelected -> + if (isSelected && amountFieldUM is SwapAmountFieldUM.Content) { + SpacerH2() EllipsisText( text = amountFieldUM.subtitle.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ellipsis = amountFieldUM.subtitleEllipsis, ) - } - } else { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.width(56.dp), - ) - TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), - ) - } - } - } -} - -@Composable -private fun SwapAmountInfoExtra( - amountUM: SwapAmountUM.Content, - amountFieldUM: SwapAmountFieldUM, - onMaxAmountClick: () -> Unit, - onSelectTokenClick: () -> Unit, -) { - when (amountFieldUM.amountType) { - SwapAmountType.From -> when (amountUM.swapDirection) { - SwapDirection.Direct -> { - AnimatedVisibility( - visible = amountUM.selectedAmountType == amountFieldUM.amountType, - enter = fadeIn(), - exit = fadeOut(), - ) { - AmountMaxButton(onMaxAmountClick) - } - } - SwapDirection.Reverse -> { - SwapAmountInfoQuote( - quoteUM = amountUM.selectedQuote, - swapRateType = amountUM.swapRateType, - onSelectTokenClick = onSelectTokenClick, - ) - } - } - - SwapAmountType.To -> when (amountUM.swapDirection) { - SwapDirection.Direct -> { - SwapAmountInfoQuote( - quoteUM = amountUM.selectedQuote, - swapRateType = amountUM.swapRateType, - onSelectTokenClick = onSelectTokenClick, - ) - } - SwapDirection.Reverse -> { - AnimatedVisibility( - visible = amountUM.selectedAmountType == amountFieldUM.amountType, - enter = fadeIn(), - exit = fadeOut(), - ) { - AmountMaxButton(onMaxAmountClick) + } else { + AnimatedContent(selectedQuote) { quote -> + when (quote) { + is SwapQuoteUM.Content -> { + SpacerH2() + EllipsisText( + text = stringResourceSafe( + R.string.send_with_swap_recipient_get_amount, + quote.quoteAmountValue.resolveReference(), + ), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + SwapQuoteUM.Loading -> { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + else -> Unit + } } } } @@ -353,7 +338,7 @@ private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { modifier = Modifier .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.background.secondary) + .background(TangemTheme.colors.button.secondary) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), @@ -364,11 +349,11 @@ private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { } @Composable -private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateType, onSelectTokenClick: () -> Unit) { +private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM?, isFixedRate: Boolean, onSelectTokenClick: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.conditionalCompose( - condition = swapRateType == ExpressRateType.Fixed, + condition = isFixedRate, modifier = { clickable( interactionSource = remember { MutableInteractionSource() }, @@ -378,29 +363,20 @@ private fun SwapAmountInfoQuote(quoteUM: SwapQuoteUM, swapRateType: ExpressRateT }, ), ) { - when (quoteUM) { - is SwapQuoteUM.Content -> EllipsisText( - text = quoteUM.quoteAmountValue.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(end = 2.dp), - ) - - is SwapQuoteUM.Error, - is SwapQuoteUM.Empty, - -> Box(modifier = Modifier.padding(start = 16.dp)) - - is SwapQuoteUM.Loading -> CircularProgressIndicator( - color = TangemTheme.colors.icon.inactive, - modifier = Modifier - .padding(end = 4.dp) - .size(20.dp), - ) - - is SwapQuoteUM.Allowance -> Text( - text = "ALLOWANCE NOT IMPLEMENTED", - ) + AnimatedContent( + quoteUM, + ) { quote -> + when (quote) { + is SwapQuoteUM.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier + .padding(end = 4.dp) + .size(20.dp), + ) + else -> Box(modifier = Modifier.padding(start = 16.dp)) + } } + Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_chevron_24), From 9160b62495167d8bb9f8a08fcc4ca944c5db183c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 17:40:24 +0500 Subject: [PATCH 092/165] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheet.kt | 108 +++++++++--------- .../modal/TangemModalBottomSheetWithFooter.kt | 2 + .../bottomsheets/sheet/TangemBottomSheet.kt | 2 + .../com/tangem/core/ui/res/TangemColors.kt | 19 +++ .../com/tangem/core/ui/res/TangemTheme.kt | 8 ++ 5 files changed, 88 insertions(+), 51 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt index ff085bffb5..dc68d70134 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -179,6 +179,7 @@ inline fun BasicModalBottomSheet( onBack = onBack, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -190,6 +191,7 @@ inline fun BasicModalBottomSheet( contentWindowInsets = { WindowInsetsZero }, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } @@ -200,58 +202,62 @@ inline fun BasicModalBottomSheet( @Composable private fun TangemModalBottomSheet_Preview() { TangemThemePreview { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TangemBottomSheetConfigContentPreviewConfig(), - ), - title = { - TangemModalBottomSheetTitle( - endIconRes = R.drawable.ic_close_24, - onEndClick = {}, - ) - }, - content = { - Column( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - modifier = Modifier - .size(56.dp) - .clip(RoundedCornerShape(100)) - .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) - .padding(12.dp), - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_alert_24), - ), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, + Box( + Modifier.background(TangemTheme.colors.background.tertiary), + ) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContentPreviewConfig(), + ), + title = { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, ) - SpacerH24() - Text( - text = "Unsuported networks", - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = "Tangem does not currently support a required network by React App.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH(48.dp) - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = "Go it", - onClick = {}, - ) - } - }, - ) + }, + content = { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100)) + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) + .padding(12.dp), + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_alert_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerH24() + Text( + text = "Unsuported networks", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = "Tangem does not currently support a required network by React App.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH(48.dp) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Go it", + onClick = {}, + ) + } + }, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 643453a4bc..9964ac424d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -235,6 +235,7 @@ inline fun BasicModalBottomSheetWit onBack = onBack, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -246,6 +247,7 @@ inline fun BasicModalBottomSheetWit contentWindowInsets = { WindowInsetsZero }, dragHandle = null, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt index 177cd551ad..1ecce16ab7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt @@ -192,6 +192,7 @@ inline fun BasicBottomSheet( dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, onBack = onBack, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } else { ModalBottomSheet( @@ -203,6 +204,7 @@ inline fun BasicBottomSheet( contentWindowInsets = { WindowInsetsZero }, dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, content = bsContent, + scrimColor = TangemTheme.colors.overlay.secondary, ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 2a78787608..12f15c13b5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -16,6 +16,7 @@ class TangemColors internal constructor( control: Control, stroke: Stroke, field: Field, + overlay: Overlay, ) { var text by mutableStateOf(text) private set @@ -31,6 +32,7 @@ class TangemColors internal constructor( private set var field by mutableStateOf(field) private set + var overlay by mutableStateOf(overlay) @Stable class Text internal constructor( @@ -220,6 +222,22 @@ class TangemColors internal constructor( } } + @Stable + class Overlay internal constructor( + primary: Color, + secondary: Color, + ) { + var primary by mutableStateOf(primary) + private set + var secondary by mutableStateOf(secondary) + private set + + fun update(other: Overlay) { + primary = other.primary + secondary = other.secondary + } + } + fun update(other: TangemColors) { text.update(other.text) icon.update(other.icon) @@ -228,5 +246,6 @@ class TangemColors internal constructor( control.update(other.control) stroke.update(other.stroke) field.update(other.field) + overlay.update(other.overlay) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index bc5f89a78c..696d1619ee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -238,6 +238,10 @@ private fun lightThemeColors(): TangemColors { primary = TangemColorPalette.Light1, focused = TangemColorPalette.Light2, ), + overlay = TangemColors.Overlay( + primary = TangemColorPalette.Black.copy(alpha = 0.4f), + secondary = TangemColorPalette.Black.copy(alpha = 0.7f), + ), ) } @@ -288,6 +292,10 @@ private fun darkThemeColors(): TangemColors { primary = TangemColorPalette.Dark5, focused = TangemColorPalette.Dark4, ), + overlay = TangemColors.Overlay( + primary = TangemColorPalette.Black.copy(alpha = 0.4f), + secondary = TangemColorPalette.Black.copy(alpha = 0.7f), + ), ) } From 2d7f146efcab0bdd32788bb38c589051dbbb43d4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 17:53:22 +0500 Subject: [PATCH 093/165] Updated on 2026-08-14 --- .../ui/SwapChooseProviderBottomSheet.kt | 10 +++- .../ui/SwapChooseProviderContent.kt | 52 ++++++++++++++----- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index dcd529b3f9..9772ead6c8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign @@ -25,6 +26,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe @@ -36,6 +39,8 @@ import com.tangem.features.swap.v2.impl.chooseprovider.ui.preview.SwapChooseProv import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM +private const val DISABLED_COLORS_ALPHA = 0.5f + @Composable internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { TangemModalBottomSheet( @@ -90,7 +95,10 @@ internal fun SwapChooseProviderContent( enabled = provider.quote !is SwapQuoteUM.Error, onClick = { onProviderClick(provider.quote) }, ) - .padding(12.dp), + .padding(12.dp) + .conditional(provider.providerUM.extraUM is ProviderChooseUM.ExtraUM.Error) { + Modifier.alpha(DISABLED_COLORS_ALPHA) + }, ) } Icon( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 0e00951292..6751315374 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -2,8 +2,7 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui import android.content.res.Configuration import androidx.compose.animation.* -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -26,6 +25,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout @@ -69,7 +69,7 @@ fun SwapChooseProviderContent( color = TangemTheme.colors.stroke.primary, modifier = Modifier.padding(horizontal = 12.dp), ) - Row(verticalAlignment = Alignment.CenterVertically) { + Row { Icon( painter = rememberVectorPainter( ImageVector.vectorResource(R.drawable.ic_stack_new_24), @@ -80,12 +80,17 @@ fun SwapChooseProviderContent( ) Text( text = stringResourceSafe(R.string.express_provider), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = 8.dp, top = 12.dp, bottom = 12.dp), ) SpacerWMax() - ProviderInfo(expressProvider, isBestRate, showBestRateAnimation, onFinishAnimation) + ProviderInfo( + expressProvider = expressProvider, + isBestRate = isBestRate, + showBestRateAnimation = showBestRateAnimation, + onFinishAnimation = onFinishAnimation, + ) } if (showFCAWarning) { FcaProviderWarning( @@ -122,8 +127,9 @@ private fun ProviderInfo( isBestRate: Boolean, showBestRateAnimation: Boolean, onFinishAnimation: () -> Unit, + modifier: Modifier = Modifier, ) { - ConstraintLayout { + ConstraintLayout(modifier = modifier) { val (imageRef, nameRef, iconRef) = createRefs() SubcomposeAsyncImage( model = ImageRequest.Builder(context = LocalContext.current) @@ -146,13 +152,13 @@ private fun ProviderInfo( .clip(RoundedCornerShape(4.dp)) .constrainAs(imageRef) { start.linkTo(parent.start) - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) + top.linkTo(parent.top, 14.dp) + bottom.linkTo(parent.bottom, 14.dp) }, ) Text( text = expressProvider?.name.orEmpty(), - style = TangemTheme.typography.body2, + style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, modifier = Modifier .padding(start = 6.dp) @@ -164,7 +170,7 @@ private fun ProviderInfo( ) Icon( painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_chevron_24), + ImageVector.vectorResource(R.drawable.ic_select_18_24), ), tint = TangemTheme.colors.icon.informative, contentDescription = null, @@ -196,6 +202,7 @@ private fun ConstraintLayoutScope.BestRateBadge( modifier: Modifier = Modifier, ) { val animateState = remember { MutableTransitionState(false) } + val animationSpec = rememberBestRateAnimationSpec(animateState) LaunchedEffect(showBestRateAnimation) { if (showBestRateAnimation) { @@ -209,6 +216,7 @@ private fun ConstraintLayoutScope.BestRateBadge( val iconSize by animateDpAsState( label = "iconSize", + animationSpec = animationSpec, targetValue = if (animateState.targetState) { 12.dp } else { @@ -217,6 +225,7 @@ private fun ConstraintLayoutScope.BestRateBadge( ) val iconVerticalPaddings by animateDpAsState( label = "iconVerticalPaddings", + animationSpec = animationSpec, targetValue = if (animateState.targetState) { 3.dp } else { @@ -225,6 +234,7 @@ private fun ConstraintLayoutScope.BestRateBadge( ) val iconHorizontalPaddings by animateDpAsState( label = "iconHorizontalPaddings", + animationSpec = animationSpec, targetValue = if (animateState.targetState) { 4.dp } else { @@ -234,6 +244,7 @@ private fun ConstraintLayoutScope.BestRateBadge( val startMargin by animateDpAsState( label = "startMargin", + animationSpec = animationSpec, targetValue = if (animateState.targetState) { (-12).dp } else { @@ -243,6 +254,7 @@ private fun ConstraintLayoutScope.BestRateBadge( val topMargin by animateDpAsState( label = "topMargin", + animationSpec = animationSpec, targetValue = if (animateState.targetState) { (-12).dp } else { @@ -273,8 +285,10 @@ private fun ConstraintLayoutScope.BestRateBadge( ) AnimatedVisibility( visibleState = animateState, - enter = expandIn() + fadeIn(), - exit = shrinkOut() + fadeOut(), + enter = expandIn(animationSpec = tween(durationMillis = 400, easing = EaseInOutQuart)) + + fadeIn(animationSpec = tween(delayMillis = 50, easing = EaseInOutQuint)), + exit = shrinkOut(animationSpec = tween(durationMillis = 400, easing = EaseInOutQuint)) + + fadeOut(animationSpec = tween(delayMillis = 100, durationMillis = 200, easing = EaseInOutQuint)), label = "textAnimation", modifier = Modifier.padding(end = 6.dp), ) { @@ -287,6 +301,20 @@ private fun ConstraintLayoutScope.BestRateBadge( } } +@Composable +private fun rememberBestRateAnimationSpec(animateState: MutableTransitionState): TweenSpec = remember( + animateState, +) { + tween( + durationMillis = 400, + easing = if (animateState.targetState) { + EaseInOutQuart + } else { + EaseInOutQuint + }, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) From 863af7c546e1d7eb41fd9c5bab0e6f444acc5e0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 17:54:12 +0500 Subject: [PATCH 094/165] Updated on 2026-08-14 --- .../fromSupported/ui/SwapChooseTokenNetworkContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index b22718a200..555acd6d00 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -45,7 +45,7 @@ import kotlinx.collections.immutable.toPersistentList internal fun SwapChooseTokenNetworkBottomSheet(config: TangemBottomSheetConfig) { TangemModalBottomSheet( config = config, - containerColor = TangemTheme.colors.background.tertiary, + containerColor = TangemTheme.colors.background.primary, title = { AnimatedContent( targetState = config.content is SwapChooseTokenNetworkContentUM.Content, From 72c8150d56921a224ec04df374c1f461d992426c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 11:57:26 +0500 Subject: [PATCH 095/165] Updated on 2026-08-14 --- .../swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index d9adf89a0c..fe05161fec 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -9,6 +9,7 @@ import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.utils.extensions.isZero import com.tangem.utils.isNullOrZero import java.math.BigDecimal import java.math.RoundingMode @@ -32,7 +33,9 @@ internal object SwapAmountQuoteUtils { secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate } - if (fromRate.isNullOrZero() || toRate.isNullOrZero()) return null + val isRatesNull = fromRate.isNullOrZero() || toRate.isNullOrZero() + val isAmountNull = fromTokenAmount.isZero() || toTokenAmount.isZero() + if (isRatesNull || isAmountNull) return null val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) val toTokenFiatValue = toTokenAmount.multiply(toRate) From ba6260e0636d86283469a76c3b1aa7d0071cd10e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 11:57:50 +0500 Subject: [PATCH 096/165] Updated on 2026-08-14 --- .../features/swap/v2/impl/amount/model/SwapAmountModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 4512f765ac..3c4c055991 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -197,8 +197,10 @@ internal class SwapAmountModel @Inject constructor( value = value, ), ) + quoteTaskScheduler.cancelTask() amountDebouncer.debounce( coroutineScope = modelScope, + waitMs = DEBOUNCE_AMOUNT_DELAY, destinationFunction = { startLoadingQuotesTask(isSilentReload = false) }, @@ -686,7 +688,7 @@ internal class SwapAmountModel @Inject constructor( } private companion object { - const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val DEBOUNCE_AMOUNT_DELAY = 500L const val QUOTES_UPDATE_DELAY = 10000L } } \ No newline at end of file From 948ed18ca6fbe064bd547b708637e91868a17640 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:29:51 +0700 Subject: [PATCH 097/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 + .../com/tangem/common/routing/AppRoute.kt | 5 + .../src/main/res/drawable/ic_archive_24.xml | 13 + .../account/AccountDetailsComponent.kt | 11 + .../com/tangem/features/account/common/UM.kt | 18 ++ .../createedit/AccountCreateEditModel.kt | 5 +- .../createedit/entity/AccountCreateEditUM.kt | 3 +- .../entity/AccountCreateEditUMBuilder.kt | 11 +- .../createedit/ui/AccountCreateEditContent.kt | 5 +- .../account/details/AccountDetailsModel.kt | 85 +++++++ .../details/DefaultAccountDetailsComponent.kt | 40 +++ .../details/di/AccountDetailsModule.kt | 27 ++ .../details/entity/AccountDetailsUM.kt | 12 + .../details/ui/AccountDetailsContent.kt | 236 ++++++++++++++++++ 14 files changed, 471 insertions(+), 11 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_archive_24.xml create mode 100644 features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 8c05bbd7b2..49f217bf18 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -9,6 +9,7 @@ import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent @@ -88,6 +89,7 @@ internal class ChildFactory @Inject constructor( private val sendComponentFactoryV2: SendComponent.Factory, private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory, private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory, + private val accountDetailsComponentFactory: AccountDetailsComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, @@ -528,6 +530,15 @@ internal class ChildFactory @Inject constructor( componentFactory = accountCreateEditComponentFactory, ) } + is AppRoute.AccountDetails -> { + createComponentChild( + context = context, + params = AccountDetailsComponent.Params( + account = route.account, + ), + componentFactory = accountDetailsComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 304f1cb956..e0d6d80408 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -336,4 +336,9 @@ sealed class AppRoute(val path: String) : Route { data class EditAccount( val account: Account, ) : AppRoute(path = "/edit_account/${account.accountId.value}") + + @Serializable + data class AccountDetails( + val account: Account, + ) : AppRoute(path = "/account_details/${account.accountId.value}") } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_archive_24.xml b/core/ui/src/main/res/drawable/ic_archive_24.xml new file mode 100644 index 0000000000..919033523a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_archive_24.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt new file mode 100644 index 0000000000..d1c47280ea --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.account.Account + +interface AccountDetailsComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val account: Account) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt b/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt new file mode 100644 index 0000000000..299fb679dc --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.account.common + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color +import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon + +data class CryptoPortfolioIconUM( + val value: Icon, + val color: Color, +) { + constructor(domainModel: CryptoPortfolioIcon) : this( + value = domainModel.value, + color = domainModel.color, + ) +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index ebd9dd3337..b4763b9c92 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -15,6 +15,7 @@ import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon @@ -74,7 +75,7 @@ internal class AccountCreateEditModel @Inject constructor( private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { val state = uiState.value val name = AccountName(state.account.name).getOrNull() ?: return - val icon = state.account.portfolioIcon + val icon = state.account.portfolioIcon.toDomain() addCryptoPortfolioUseCase( userWalletId = params.userWalletId, accountName = name, @@ -86,7 +87,7 @@ internal class AccountCreateEditModel @Inject constructor( private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) { val state = uiState.value val name = AccountName(state.account.name).getOrNull() ?: return - val icon = state.account.portfolioIcon + val icon = state.account.portfolioIcon.toDomain() val isNewName = name != params.account.name val isNewIcon = icon != params.account.portfolioIcon updateCryptoPortfolioUseCase( diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 2c7fd2d0f0..4b133a4d97 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.account.createedit.entity import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList data class AccountCreateEditUM( @@ -15,7 +16,7 @@ data class AccountCreateEditUM( data class Account( val name: String, - val portfolioIcon: CryptoPortfolioIcon, + val portfolioIcon: CryptoPortfolioIconUM, val derivationInfo: TextReference, val inputPlaceholder: TextReference, val onNameChange: (String) -> Unit, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 75f31f86ac..41f12322bb 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent +import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @@ -15,7 +16,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor( private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList() - private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount() + private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() val toolbarTitle: TextReference get() = when (params) { @@ -34,7 +35,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor( ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( name = params.account.name.value, - portfolioIcon = params.account.portfolioIcon, + portfolioIcon = params.account.portfolioIcon.toUM(), derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ? inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), onNameChange = onNameChange, @@ -86,8 +87,7 @@ internal class AccountCreateEditUMBuilder @Inject constructor( } fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM { - val newIcon = CryptoPortfolioIcon.ofCustomAccount( - value = account.portfolioIcon.value, + val newIcon = this.account.portfolioIcon.copy( color = color, ) return this.copy( @@ -97,9 +97,8 @@ internal class AccountCreateEditUMBuilder @Inject constructor( } fun AccountCreateEditUM.updateIconSelect(icon: CryptoPortfolioIcon.Icon): AccountCreateEditUM { - val newIcon = CryptoPortfolioIcon.ofCustomAccount( + val newIcon = this.account.portfolioIcon.copy( value = icon, - color = account.portfolioIcon.color, ) return this.copy( account = this.account.copy(portfolioIcon = newIcon), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 725964567d..2f98a553bc 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -39,6 +39,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.toUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account import kotlinx.collections.immutable.toImmutableList @@ -298,7 +299,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun onEditAccountClick() { + router.push(AppRoute.EditAccount(params.account)) + } + + private fun onManageTokensClick() { + // todo account add account param + router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS)) + } + + private fun onArchiveAccountClick() { + confirmArchiveDialog() + } + + private fun confirmArchiveDialog() { + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_details_archive_action), + warning = true, + onClick = ::archiveCryptoPortfolio, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_details_archive), + message = resourceReference(R.string.account_details_archive_description), + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun archiveCryptoPortfolio() = modelScope.launch { + archiveCryptoPortfolioUseCase(params.account.accountId) + } + + private fun getInitialState(): AccountDetailsUM { + return AccountDetailsUM( + accountName = params.account.name.value, + accountIcon = params.account.portfolioIcon.toUM(), + onCloseClick = { router.pop() }, + onAccountEditClick = ::onEditAccountClick, + onManageTokensClick = ::onManageTokensClick, + onArchiveAccountClick = ::onArchiveAccountClick, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt new file mode 100644 index 0000000000..5051ca3134 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/DefaultAccountDetailsComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.details + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.details.ui.AccountDetailsContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultAccountDetailsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: AccountDetailsComponent.Params, +) : AppComponentContext by appComponentContext, AccountDetailsComponent { + + private val model: AccountDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + AccountDetailsContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : AccountDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: AccountDetailsComponent.Params, + ): DefaultAccountDetailsComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt new file mode 100644 index 0000000000..fdc4feda8a --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/di/AccountDetailsModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.details.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.AccountDetailsComponent +import com.tangem.features.account.details.AccountDetailsModel +import com.tangem.features.account.details.DefaultAccountDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountDetailsModule { + + @Binds + fun bindAccountDetailsComponentFactory( + impl: DefaultAccountDetailsComponent.Factory, + ): AccountDetailsComponent.Factory + + @Binds + @IntoMap + @ClassKey(AccountDetailsModel::class) + fun bindAccountDetailsModel(model: AccountDetailsModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt new file mode 100644 index 0000000000..7b2bc9b44d --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.account.details.entity + +import com.tangem.features.account.common.CryptoPortfolioIconUM + +data class AccountDetailsUM( + val accountName: String, + val accountIcon: CryptoPortfolioIconUM, + val onCloseClick: () -> Unit, + val onAccountEditClick: () -> Unit, + val onManageTokensClick: () -> Unit, + val onArchiveAccountClick: () -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt new file mode 100644 index 0000000000..6234a1001b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -0,0 +1,236 @@ +package com.tangem.features.account.details.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.R +import com.tangem.common.ui.account.getResId +import com.tangem.common.ui.account.getUiColor +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.AutoSizeTextField +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.common.CryptoPortfolioIconUM +import com.tangem.features.account.common.toUM +import com.tangem.features.account.details.entity.AccountDetailsUM + +@Composable +internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16) + .weight(1f), + + ) { + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = stringResourceSafe(R.string.account_details_title), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + SpacerH16() + AccountRow(state) + SpacerH16() + ManageTokensRow(state) + SpacerH16() + ArchiveAccountRow(state) + SpacerH(8.dp) + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.account_details_archive_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun ArchiveAccountRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onArchiveAccountClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + tint = TangemTheme.colors.icon.warning, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24), + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.account_details_archive), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.subtitle1, + ) + } +} + +@Composable +private fun ManageTokensRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onManageTokensClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_group_24), + tint = TangemTheme.colors.icon.secondary, + contentDescription = null, + ) + Text( + text = stringResourceSafe(R.string.main_manage_tokens), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AccountRow(state: AccountDetailsUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) + .background(TangemTheme.colors.background.primary) + .clickable(onClick = state.onAccountEditClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)), + accountName = state.accountName, + accountIcon = state.accountIcon, + ) + Column( + modifier = Modifier + .weight(1f), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = stringResourceSafe(R.string.account_form_name), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + AutoSizeTextField( + textStyle = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + value = state.accountName, + singleLine = true, + readOnly = true, + onValueChange = {}, + ) + } + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.common_edit), + onClick = state.onAccountEditClick, + ), + ) + } +} + +// todo account make reusable +@Composable +private fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier.background(accountIcon.color.getUiColor()), + ) { + val icon = accountIcon.value + val letter = accountName.first() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter.uppercase(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = Modifier.size(20.dp), + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountDetailsUM) { + TangemThemePreview { + AccountDetailsContent(state = params) + } +} + +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val first = AccountDetailsUM( + onCloseClick = {}, + onAccountEditClick = {}, + onManageTokensClick = {}, + onArchiveAccountClick = {}, + accountName = "Main", + accountIcon = portfolioIcon, + ) + add(first) + portfolioIcon = portfolioIcon.copy( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.entries.random(), + ) + add(first.copy(accountIcon = portfolioIcon)) + }, +) \ No newline at end of file From 57439e1682a0ef000178a6753f78ea21adba22e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:36:06 +0500 Subject: [PATCH 098/165] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 1 + core/res/src/main/res/values-es/strings.xml | 1 + core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 4 ++++ 7 files changed, 10 insertions(+) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7bc007eb36..554f254397 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -118,6 +118,7 @@ Zugang verweigert Zum Portfolio hinzufügen Token hinzufügen + Vertragsadresse Alle Erlauben Betrag diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8d38231daf..b5eeeccbc8 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -115,6 +115,7 @@ Acceso denegado Añadir al portafolio Agregar token + Dirección Todos Autorizar Montante diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 00b0250a97..8e7d9898e8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -97,6 +97,7 @@ Accès refusé Ajouter au portfolio Ajouter un jeton + Adresse Tous Permettre Montant diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ff32b8bf8b..2986582d9d 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -146,6 +146,7 @@ アクセスが拒否されました ポートフォリオに追加 トークンを追加 + アドレス すべて 許可する 金額 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5b238275be..32374c0f84 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -94,6 +94,7 @@ Доступ запрещен Добавить в портфель Добавить токен + Адрес Все Разрешить Сумма diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 79d1d3c092..0b17da86ca 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -94,6 +94,7 @@ Доступ заборонено Додати у портфель Додати токен + Адреса Усе Дозволити Сума diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..f0624f96ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name @@ -151,6 +152,7 @@ Access denied Add to portfolio Add token + Address All Allow Amount @@ -1001,6 +1003,8 @@ Swap and send Proceed with conversion? This will clear your previous data. Confirm Conversion + Sending any other currency will result in its irreversible loss. + Select the correct recipient network Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive To recipient From 2d96d77fe39e3ecf4712476ef300974c27c9441d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:42:16 +0500 Subject: [PATCH 099/165] Updated on 2026-08-14 --- .../com/tangem/features/send/v2/send/DefaultSendComponent.kt | 2 +- .../swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt | 2 +- .../walletconnect/transaction/ui/common/WcAddressItem.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 47c52407c5..9ac55d3a43 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -156,7 +156,7 @@ internal class DefaultSendComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index ed82f2f734..bc241e0ec0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -160,7 +160,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt index 35e9a28c76..b8c0eb3057 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt @@ -30,7 +30,7 @@ internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.wc_common_address), + text = stringResourceSafe(R.string.common_address), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, From c469d4133eba619b70b42a2a35ec1462a90b20a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:43:24 +0500 Subject: [PATCH 100/165] Updated on 2026-08-14 --- .../subcomponents/destination/analytics/EnterAddressSource.kt | 3 +++ .../v2/subcomponents/destination/model/SendDestinationModel.kt | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt index 489d2d0bc6..7dc514bb33 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -10,4 +10,7 @@ internal enum class EnterAddressSource { val isPasted: Boolean get() = this != InputField + + val isAutoNext: Boolean + get() = this == RecentAddress || this == MyWallets } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index b457df2b9a..d68153cd9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -286,8 +286,7 @@ internal class SendDestinationModel @Inject constructor( } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) { + if (type?.isAutoNext == true && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } From c68cf7165b47bed6eebffa735a6b3ed34f7fb7f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 12:51:27 +0300 Subject: [PATCH 101/165] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index 428b83bb37..794a8187e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 428b83bb378b615209e23afa05c88c454d06a9f1 +Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a From 2557efe5434ca7c9c1943b54b42b140c6972a718 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:21:14 +0500 Subject: [PATCH 102/165] Updated on 2026-08-14 --- .../ui/amountScreen/converters/AmountStateConverter.kt | 2 ++ .../converters/field/AmountBoundaryUpdateTransformer.kt | 1 + .../com/tangem/common/ui/amountScreen/models/AmountState.kt | 4 +++- .../common/ui/amountScreen/preview/AmountStatePreviewData.kt | 3 ++- .../com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt | 2 +- .../SendConfirmationNotificationsTransformerTest.kt | 1 + .../SendConfirmationNotificationsTransformerV2Test.kt | 1 + .../confirm/model/transformers/TransformersComparisonTest.kt | 1 + .../swap/v2/impl/amount/ui/SwapAmountBlockContent.kt | 5 ++--- 9 files changed, 14 insertions(+), 6 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index ac0ebecf2f..2b60066ea7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -60,6 +60,7 @@ class AmountStateConverter( return AmountState.Data( title = value.title, availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + availableBalanceShort = stringReference(crypto), tokenName = stringReference(status.currency.name), tokenIconState = iconStateConverter.convert(status), amountTextField = amountFieldConverter.convert(value.value), @@ -130,6 +131,7 @@ class AmountStateConverterV2( } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) }, + availableBalanceShort = stringReference(crypto), tokenName = stringReference(cryptoCurrencyStatus.currency.name), tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency), amountTextField = amountFieldConverter.convert(value.value), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 89c1074a49..7385fa9542 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -48,6 +48,7 @@ class AmountBoundaryUpdateTransformer( return prevState.copy( availableBalance = availableBalance, + availableBalanceShort = stringReference(crypto), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index cc23669f47..cac4628783 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -17,7 +17,8 @@ sealed class AmountState { /** * @param isPrimaryButtonEnabled indicates if next state button enabled * @param title title - * @param availableBalance user crypto currency balance + * @param availableBalance user crypto currency balance with fiat balance + * @param availableBalanceShort user crypto currency balance without fiat balance * @param tokenIconState crypto currency icon state * @param segmentedButtonConfig currency switcher config * @param selectedButton selected currency index @@ -33,6 +34,7 @@ sealed class AmountState { override val isRedesignEnabled: Boolean, val title: TextReference, val availableBalance: TextReference, + val availableBalanceShort: TextReference, val tokenName: TextReference, val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 99c11a9a14..a2db25c4dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -24,7 +24,8 @@ object AmountStatePreviewData { val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), - availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), + availableBalance = stringReference("2 130,88 USDT • 2 129,92 \$)"), + availableBalanceShort = stringReference("2 130,88 USDT"), tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index ee080d579f..979a1a4419 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -64,7 +64,7 @@ fun AmountBlockV2( AmountBlockV2( title = amountState.title, - balance = amountState.availableBalance, + balance = amountState.availableBalanceShort, currencyTitle = currencyTitle, currencyIconState = amountState.tokenIconState, firstAmount = firstAmount, diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt index 6eece69628..020d3afcab 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt @@ -205,6 +205,7 @@ class SendConfirmationNotificationsTransformerTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 873cfb305a..6903fc2550 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -204,6 +204,7 @@ class SendConfirmationNotificationsTransformerV2Test { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt index 79cba32b78..b07143a6b1 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt @@ -307,6 +307,7 @@ class TransformersComparisonTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b788f33190..c68f7e3923 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -72,14 +72,13 @@ internal fun SwapAmountBlockContent( start.linkTo(parent.start) end.linkTo(parent.end) }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) - }, + extraContent = { SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) }, ) AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( title = resourceReference(R.string.send_with_swap_recipient_amount_title), availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, ) ?: amountUM.secondaryAmount.amountField, isClickDisabled = true, isEditingDisabled = false, From bea775e94bc0b8b5c8a9c24403612568b715eede Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:47:48 +0300 Subject: [PATCH 103/165] Updated on 2026-08-14 --- .../java/com/tangem/tap/LockTimerWorker.kt | 17 ++- .../com/tangem/tap/LockUserWalletsTimer.kt | 35 ++++-- .../main/java/com/tangem/tap/MainActivity.kt | 68 +++++++++++ .../tap/data/RuntimeUserWalletsStore.kt | 5 - .../data/UserWalletsStoreRepositoryProxy.kt | 50 ++++++++ .../tap/di/data/UserWalletsStoreModule.kt | 15 ++- .../tangem/tap/di/domain/CardDomainModule.kt | 15 ++- .../tap/di/domain/CardLegacyDomainModule.kt | 14 ++- .../tap/di/domain/MarketsDomainModule.kt | 6 + .../tap/di/domain/WalletsDomainModule.kt | 115 +++++++++++++++--- .../DefaultUserWalletsListRepository.kt | 25 ++-- .../UserWalletEncryptionKeysRepository.kt | 17 +-- .../di/WalletConnectInteractorModule.kt | 6 +- .../domain/WalletConnectInteractor.kt | 7 +- .../tap/network/auth/DefaultAuthProvider.kt | 35 ++++-- .../tangem/tap/network/auth/di/AuthModule.kt | 14 ++- .../di/FeatureTogglesManagerModule.kt | 7 ++ .../feature/impl/DevFeatureTogglesManager.kt | 23 ++-- .../feature/impl/ProdFeatureTogglesManager.kt | 11 +- .../datasource/api/common/AuthProvider.kt | 6 +- .../local/userwallet/UserWalletsStore.kt | 6 +- .../managers/ProdApiConfigsManagerTest.kt | 5 +- .../wallets/DefaultWalletsRepositoryTest.kt | 2 +- ...FilterAvailableNetworksForWalletUseCase.kt | 12 +- .../DefaultUserWalletsSyncDelegate.kt | 38 +++++- .../wallets/models/SelectWalletError.kt | 6 - .../wallets/usecase/DeleteWalletUseCase.kt | 13 +- .../usecase/GenerateWalletNameUseCase.kt | 16 ++- .../usecase/GetSavedWalletsCountUseCase.kt | 7 ++ .../usecase/GetSelectedWalletSyncUseCase.kt | 13 +- .../usecase/GetSelectedWalletUseCase.kt | 26 +++- .../wallets/usecase/GetUserWalletUseCase.kt | 23 +++- .../wallets/usecase/GetWalletNamesUseCase.kt | 14 ++- .../wallets/usecase/GetWalletsUseCase.kt | 22 +++- .../wallets/usecase/IsNeedToBackupUseCase.kt | 17 ++- .../wallets/usecase/SaveWalletUseCase.kt | 56 +++++++-- .../wallets/usecase/SelectWalletUseCase.kt | 12 +- .../wallets/usecase/UpdateWalletUseCase.kt | 36 +++++- .../GetSavedWalletsCountUseCaseTest.kt | 6 +- .../biometry/impl/model/AskBiometryModel.kt | 6 +- .../model/MultiWalletFinalizeModel.kt | 16 +-- .../v2/twin/impl/model/OnboardingTwinModel.kt | 31 +++-- .../model/OnboardingVisaInProgressModel.kt | 6 +- .../feature/swap/DefaultSwapRepository.kt | 8 +- .../tangem/feature/swap/di/SwapDataModule.kt | 6 +- .../domain/WalletNameMigrationUseCase.kt | 33 +++-- 46 files changed, 749 insertions(+), 178 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index e51fc573ea..f6b3c93141 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -7,6 +7,8 @@ import androidx.work.WorkerParameters import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedInject import timber.log.Timber @@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor( @Assisted params: WorkerParameters, private val settingsRepository: SettingsRepository, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { Timber.i("onStart job") - val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() - userWalletsListManagerLockable.lock() - settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.lockAllWallets() + .onRight { + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } + } else { + val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() + userWalletsListManagerLockable.lock() + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } Timber.i("onStart job complete") return Result.success() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 57d5d8c6ca..e3d0a18067 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.CoroutineScope @@ -25,6 +27,8 @@ internal class LockUserWalletsTimer( private val settingsRepository: SettingsRepository, private val duration: Duration = with(Duration) { 5.minutes }, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val coroutineScope: CoroutineScope, ) : LifecycleOwner by context as LifecycleOwner, DefaultLifecycleObserver { @@ -108,20 +112,33 @@ internal class LockUserWalletsTimer( delay(duration) - val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch + if (hotWalletFeatureToggles.isHotWalletEnabled) { + val userWallets = userWalletsListRepository.userWalletsSync() + if (userWallets.isNotEmpty()) { + userWalletsListRepository.lockAllWallets() + .onLeft { + start() + } + .onRight { + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } + } + } else { + val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch - if (userWalletsListManager.hasUserWallets) { - val currentTime = System.currentTimeMillis() + if (userWalletsListManager.hasUserWallets) { + val currentTime = System.currentTimeMillis() - Timber.i( - """ + Timber.i( + """ Finished |- Millis passed: ${currentTime - startTime} - """.trimIndent(), - ) + """.trimIndent(), + ) - userWalletsListManager.lock() - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + userWalletsListManager.lock() + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index fa9d503de0..2f037143f3 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase @@ -48,7 +49,9 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper @@ -188,6 +191,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler + @Inject + internal lateinit var userWalletsListRepository: UserWalletsListRepository + + @Inject + internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -271,6 +280,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { settingsRepository = settingsRepository, userWalletsListManager = userWalletsListManager, coroutineScope = mainScope, + userWalletsListRepository = userWalletsListRepository, + hotWalletFeatureToggles = hotWalletFeatureToggles, ) initIntentHandlers() @@ -429,6 +440,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { + // TODO refactor this method to return a route instead of navigating directly + if (hotWalletFeatureToggles.isHotWalletEnabled) { + navigateToInitialScreenIfNeededNew(intentWhichStartedActivity) + return + } + val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial } @@ -448,6 +465,57 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + @Deprecated("Refactor this method to return a route instead of navigating directly") + private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) { + lifecycleScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) + if (userWallets.isEmpty()) { + val shouldShowTos = !cardRepository.isTangemTOSAccepted() + + val route = if (shouldShowTos) { + AppRoute.Disclaimer(isTosAccepted = false) + } else { + AppRoute.Home(launchMode = launchMode) + } + + store.dispatchNavigationAction { replaceAll(route) } + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = false, + ) + } else { + if (userWallets.any { it.isLocked }) { + store.dispatchNavigationAction { + replaceAll( + AppRoute.Welcome( + launchMode = launchMode, + intent = intentWhichStartedActivity?.let(::SerializableIntent), + ), + ) + } + } else { + store.dispatchNavigationAction { + replaceAll(AppRoute.Wallet) + } + } + + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = true, + ) + } + + if (intent != null) { + handleDeepLink(intent = intent, isFromOnNewIntent = false) + } + + viewModel.checkForUnfinishedBackup() + } + } + private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index dea8dc75a4..d780f3d15a 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // [REDACTED_JIRA] @@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore( return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } } - override suspend fun getAllSyncOrNull(): List? { - return userWalletsListManager.userWallets.firstOrNull() - } - override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt new file mode 100644 index 0000000000..c5275bf034 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.data + +import com.tangem.common.CompletionResult +import com.tangem.common.catching +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class UserWalletsStoreRepositoryProxy( + private val userWalletsListRepository: UserWalletsListRepository, +) : UserWalletsStore { + + override val selectedUserWalletOrNull: UserWallet? + get() = userWalletsListRepository.selectedUserWallet.value + + override val userWallets: Flow> + get() = flow { + userWalletsListRepository.load() + userWalletsListRepository.userWallets.collect { + emit(requireNotNull(it)) + } + } + + override fun getSyncOrNull(key: UserWalletId): UserWallet? { + return userWalletsListRepository.userWallets.value?.find { it.walletId == key } + } + + override fun getSyncStrict(key: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } + } + + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return catching { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } + val updatedUserWallet = update(userWallet) + userWalletsListRepository.saveWithoutLock( + userWallet = updatedUserWallet, + canOverride = true, + ) + updatedUserWallet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index b2a04f9d52..2fe50e3705 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -2,7 +2,10 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.data.RuntimeUserWalletsStore +import com.tangem.tap.data.UserWalletsStoreRepositoryProxy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +18,15 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { - return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + fun provideUserWalletsStore( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UserWalletsStore { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + UserWalletsStoreRepositoryProxy(userWalletsListRepository) + } else { + RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 0aa400353e..a674e4ac97 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -7,11 +7,13 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase @@ -42,9 +44,16 @@ internal object CardDomainModule { } @Provides - @Singleton - fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) + fun provideIsNeedToBackupUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 7be9d95b9a..5f8a03b7d2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -3,7 +3,9 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.LegacyScanProcessor @@ -31,7 +33,15 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { - return GenerateWalletNameUseCase(userWalletsListManager) + fun providesWalletNameGenerateUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GenerateWalletNameUseCase { + return GenerateWalletNameUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 1bb8a1ec1c..1989f4868e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -13,6 +13,8 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -81,10 +83,14 @@ object MarketsDomainModule { @Singleton fun provideFilterNetworksUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, excludedBlockchains: ExcludedBlockchains, ): FilterAvailableNetworksForWalletUseCase { return FilterAvailableNetworksForWalletUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, excludedBlockchains = excludedBlockchains, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 87b107936a..eff26615ae 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -31,18 +33,30 @@ internal object WalletsDomainModule { @Provides fun providesUserWalletsSyncDelegate( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): UserWalletsSyncDelegate { return DefaultUserWalletsSyncDelegate( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, dispatchers = dispatchers, ) } @Provides @Singleton - fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { - return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletsUseCase { + return GetWalletsUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -50,37 +64,71 @@ internal object WalletsDomainModule { fun providesWalletNameMigrationUseCase( userWalletsListManager: UserWalletsListManager, walletNamesMigrationRepository: WalletNamesMigrationRepository, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): WalletNameMigrationUseCase { return WalletNameMigrationUseCase( userWalletsListManager = userWalletsListManager, walletNamesMigrationRepository = walletNamesMigrationRepository, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } @Provides @Singleton - fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { - return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetUserWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetUserWalletUseCase { + return GetUserWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) + return GetSelectedWalletSyncUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetSelectedWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { - return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesSaveWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): SaveWalletUseCase { + return SaveWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -99,15 +147,30 @@ internal object WalletsDomainModule { @Singleton fun providesSelectWalletUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { - return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) + return SelectWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + reduxStateHolder = reduxStateHolder, + ) } @Provides @Singleton - fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { - return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesUpdateWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UpdateWalletUseCase { + return UpdateWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -124,14 +187,30 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { - return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsSyncUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletNamesUseCase { + return GetWalletNamesUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { - return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesDeleteWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): DeleteWalletUseCase { + return DeleteWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -214,9 +293,13 @@ internal object WalletsDomainModule { @Singleton fun providesGetSavedWalletChangesIdUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSavedWalletsCountUseCase { return GetSavedWalletsCountUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d60a2a0f74..c1cb92a998 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -30,6 +30,7 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import com.tangem.utils.Provider import com.tangem.utils.ProviderSuspend +import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -173,6 +174,8 @@ internal class DefaultUserWalletsListRepository( userWalletEncryptionKeysRepository.delete(userWalletIds) + val userWalletsBeforeDelete = userWallets.value ?: return@either + userWallets.update { currentWallets -> currentWallets?.filterNot { it.walletId in userWalletIds } } @@ -181,7 +184,7 @@ internal class DefaultUserWalletsListRepository( if (currentSelected == null) return@update null userWallets.value?.findAvailableUserWallet( - userWallets.value?.indexOfFirst { it.walletId == currentSelected.walletId } ?: 0, + userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0, ) } } @@ -199,7 +202,7 @@ internal class DefaultUserWalletsListRepository( when (unlockMethod) { UserWalletsListRepository.UnlockMethod.Biometric -> { - unlockAllWallets() + unlockAllWallets().bind() select(userWalletId) } UserWalletsListRepository.UnlockMethod.AccessCode -> { @@ -225,7 +228,7 @@ internal class DefaultUserWalletsListRepository( } sensitiveInformationRepository.getAll(listOf(encryptionKey)) - .doOnSuccess { userWallets.value?.updateWith(it) } + .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) } @@ -255,6 +258,7 @@ internal class DefaultUserWalletsListRepository( } override suspend fun unlockAllWallets(): Either = either { + val userWalletIds = userWalletsSync().map { it.walletId }.toSet() val biometricKeys = runCatching { userWalletEncryptionKeysRepository.getAllBiometric() }.getOrElse { @@ -264,8 +268,14 @@ internal class DefaultUserWalletsListRepository( val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() val allKeys = biometricKeys + unsecuredKeys + + if (allKeys.all { it.walletId in userWalletIds }.not()) { + raise(UnlockWalletError.UnableToUnlock) + } + sensitiveInformationRepository.getAll(allKeys) - .doOnSuccess { userWallets.value?.updateWith(it) } + .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } override suspend fun lockAllWallets(): Either = either { @@ -297,7 +307,7 @@ internal class DefaultUserWalletsListRepository( biometryFallback: suspend () -> Either, ): Either { val result = passwordRequester.requestPassword( - hasBiometry = tangemSdkManagerProvider.invoke().needEnrollBiometrics, + hasBiometry = tangemSdkManagerProvider.invoke().canUseBiometry, ) return when (result) { @@ -312,6 +322,7 @@ internal class DefaultUserWalletsListRepository( requestPasswordRecursive(block, biometryFallback) } else { passwordRequester.successfulAuthentication() + passwordRequester.dismiss() decrypted.right() } } @@ -319,9 +330,9 @@ internal class DefaultUserWalletsListRepository( biometryFallback() .onRight { passwordRequester.successfulAuthentication() + passwordRequester.dismiss() } - passwordRequester.dismiss() - null.right() + .map { null } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index fb42970c29..43a5b4916f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -62,15 +62,16 @@ internal class UserWalletEncryptionKeysRepository( } } - suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? { - val encrypted = secureStorage.get( - account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, - ) ?: return null + suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? = + withContext(dispatchers.io) { + val encrypted = secureStorage.get( + account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, + ) ?: return@withContext null - val decrypted = AESEncryptionProtocol.decryptWithPassword(password, encrypted) - - return decrypted.decodeToKey() - } + withContext(dispatchers.default) { + AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey() + } + } suspend fun getAllBiometric(): List = withContext(dispatchers.io) { val keys = getUserWalletsIds().map { userWalletId -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 6992564847..6c9e9f189d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule { wcSessionsRepository: WalletConnectSessionsRepository, currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, walletConnectFeatureToggles: WalletConnectFeatureToggles, coroutineDispatcherProvider: CoroutineDispatcherProvider, + getSelectedWalletUseCase: GetSelectedWalletUseCase, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule { blockchainHelper = TangemWcBlockchainHelper(), currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, - userWalletsListManager = userWalletsListManager, + getSelectedWalletUseCase = getSelectedWalletUseCase, dispatchers = coroutineDispatcherProvider, walletConnectFeatureToggles = walletConnectFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index f602ed8fc4..395ad23885 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.model.legacy.Session import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.extensions.dispatchOnMain @@ -38,18 +37,14 @@ class WalletConnectInteractor( private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val userWalletsListManager: UserWalletsListManager, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, val blockchainHelper: WcBlockchainHelper, ) { private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } private var isWalletConnectReadyForDeepLinks = false - private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { - GetSelectedWalletUseCase(userWalletsListManager) - } - private val wcScope = CoroutineScope( SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 5ce4ad3c20..99425e0191 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -4,11 +4,16 @@ import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository -internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider { +internal class DefaultAuthProvider( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean = false, +) : AuthProvider { - override fun getCardPublicKey(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardPublicKey(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -17,8 +22,8 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardPublicKey.toHexString() } - override fun getCardId(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardId(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -27,9 +32,25 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardId } - override fun getCardsPublicKeys(): Map { - return userWalletsListManager.userWalletsSync.filterIsInstance().associate { + override suspend fun getCardsPublicKeys(): Map { + return getWallets().filterIsInstance().associate { it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() } } + + private suspend fun getWallets(): List { + return if (useNewListRepository) { + userWalletsListRepository.userWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } + } + + private suspend fun getSelectedWallet(): UserWallet? { + return if (useNewListRepository) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 5f724624af..95004c11d8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider @@ -22,8 +24,16 @@ internal class AuthModule { @Provides @Singleton - fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider { - return DefaultAuthProvider(userWalletsListManager) + fun provideAuthProvider( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): AuthProvider { + return DefaultAuthProvider( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 8346378b52..fa5c04f6a1 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -14,6 +14,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking import javax.inject.Singleton @Module @@ -41,6 +42,12 @@ internal object FeatureTogglesManagerModule { localTogglesStorage = localTogglesStorage, versionProvider = versionProvider, ) + }.also { + // We need to initialize during the hilt graph creation + // in order to provide the feature toggles correctly to other dependencies. + runBlocking { + it.init() + } } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 7aca4511ea..ce0d035439 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import kotlin.properties.Delegates /** * Feature toggles manager implementation in DEV build @@ -24,10 +23,14 @@ internal class DevFeatureTogglesManager( private val versionProvider: VersionProvider, ) : MutableFeatureTogglesManager { - private var featureTogglesMap: MutableMap by Delegates.notNull() - private var localFeatureTogglesMap: Map by Delegates.notNull() + private var featureTogglesMap: MutableMap? = null + private var localFeatureTogglesMap: Map? = null override suspend fun init() { + if (featureTogglesMap != null && localFeatureTogglesMap != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( @@ -46,21 +49,21 @@ internal class DevFeatureTogglesManager( .toMutableMap() } - override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap - override fun getFeatureToggles(): Map = featureTogglesMap + override fun getFeatureToggles(): Map = featureTogglesMap!! override suspend fun changeToggle(name: String, isEnabled: Boolean) { - featureTogglesMap[name] ?: return - featureTogglesMap[name] = isEnabled - appPreferencesStore.storeFeatureToggles(value = featureTogglesMap) + featureTogglesMap!![name] ?: return + featureTogglesMap!![name] = isEnabled + appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!) } override suspend fun recoverLocalConfig() { - featureTogglesMap = localFeatureTogglesMap.toMutableMap() - appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap) + featureTogglesMap = localFeatureTogglesMap!!.toMutableMap() + appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index d723297a45..54fe7a011d 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -5,7 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.storage.TogglesStorage import com.tangem.core.configtoggle.utils.associateToggles import com.tangem.core.configtoggle.version.VersionProvider -import kotlin.properties.Delegates /** * Feature toggles manager implementation in PROD build @@ -18,18 +17,22 @@ internal class ProdFeatureTogglesManager( private val versionProvider: VersionProvider, ) : FeatureTogglesManager { - private var featureToggles: Map by Delegates.notNull() + private var featureToggles: Map? = null override suspend fun init() { + if (featureToggles != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) featureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get() ?: "") } - override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getProdFeatureToggles() = featureToggles + fun getProdFeatureToggles() = featureToggles!! @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun setProdFeatureToggles(map: Map) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 7945fc59eb..da1b1aca5f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -8,12 +8,12 @@ interface AuthProvider { /** * Returns authToken for tangem tech api */ - fun getCardPublicKey(): String + suspend fun getCardPublicKey(): String - fun getCardId(): String + suspend fun getCardId(): String /** * Returns map where keys(cardId) associated with cardPublicKey */ - fun getCardsPublicKeys(): Map + suspend fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 4f1d6b5cba..b4d50f01de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +@Deprecated( + message = "Use UserWalletsListRepository instead", + replaceWith = ReplaceWith("UserWalletsListRepository"), +) interface UserWalletsStore { val selectedUserWalletOrNull: UserWallet? @@ -15,8 +19,6 @@ interface UserWalletsStore { fun getSyncStrict(key: UserWalletId): UserWallet - suspend fun getAllSyncOrNull(): List? - suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index defd5db207..61d25e29b1 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.version.AppVersionProvider import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest { every { appVersionProvider.versionName } returns VERSION_NAME every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY - every { appAuthProvider.getCardId() } returns APP_CARD_ID - every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY + coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID + coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY every { appInfoProvider.osVersion } returns "Android 16" } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 2911dd5968..447c2effdf 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -195,7 +195,7 @@ class DefaultWalletsRepositoryTest { ) val authProvider = mockk { - every { getCardsPublicKeys() } returns publicKeys + coEvery { getCardsPublicKeys() } returns publicKeys } repository = DefaultWalletsRepository( diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index f1c80f3ba0..044bc6a726 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -6,9 +6,13 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync class FilterAvailableNetworksForWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val excludedBlockchains: ExcludedBlockchains, ) { @@ -20,7 +24,7 @@ class FilterAvailableNetworksForWalletUseCase( userWalletId: UserWalletId, networks: Set, ): Set { - val userWallet = userWalletsListManager.userWalletsSync.firstOrNull { + val userWallet = getWallets().firstOrNull { it.walletId == userWalletId } ?: return networks.toSet() @@ -33,4 +37,10 @@ class FilterAvailableNetworksForWalletUseCase( supportedBlockchains.contains(blockchain) }.toSet() } + + private fun getWallets() = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index 16b0be7fa0..403ac7a76b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -10,11 +10,14 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class DefaultUserWalletsSyncDelegate( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletsSyncDelegate { @@ -28,10 +31,43 @@ class DefaultUserWalletsSyncDelegate( } } - // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, + ): Either = if (useNewRepository) { + renameUserWalletInNewRepository(userWalletId, name) + } else { + renameUserWalletInLegacyRepository(userWalletId, name) + } + + private suspend fun renameUserWalletInNewRepository( + userWalletId: UserWalletId, + name: String, + ): Either = either { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.find { it.walletId == userWalletId } + ?: raise(UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found"))) + + ensure(userWallets.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + ensure(name != userWallet.name) { + UpdateWalletError.NameAlreadyExists + } + + val updatedWallet = userWallet.copy(name = name) + + userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .map { updatedWallet } + .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) } + .bind() + } + + // TODO remove dispatchers whnen UserWalletsListManager will be main safe + private suspend fun renameUserWalletInLegacyRepository( + userWalletId: UserWalletId, + name: String, ): Either = withContext(dispatchers.io) { either { val existingNames = userWalletsListManager.userWalletsSync diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt deleted file mode 100644 index e2aeab608f..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface SelectWalletError { - - object UnableToSelectUserWallet : SelectWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index c876b7d526..c3c49c4f7e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for deleting user wallet @@ -14,7 +15,11 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class DeleteWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { /** * Deletes user wallet with provided ID. @@ -24,6 +29,12 @@ class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListMan * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. * */ suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { + userWalletsListRepository.selectedUserWallet.value != null + } + } + return either { userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) .doOnFailure { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index e779085950..fdc88856e7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -2,12 +2,16 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.ProductType import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for user wallet name generation */ class GenerateWalletNameUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { @@ -17,16 +21,24 @@ class GenerateWalletNameUseCase( isStartToCoin = isStartToCoin, ) - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } fun invokeForHot(): String { val defaultName = "Wallet" - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } + private fun getNamesSet(): Set { + return if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() + } else { + userWalletsListManager.userWalletsSync.map { it.name }.toSet() + } + } + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { val startIndex = 2 if (!existingNames.contains(defaultName)) { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 6afc79d552..32233cdbf9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -4,13 +4,20 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.* class GetSavedWalletsCountUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(): Flow> { + if (useNewRepository) { + return userWalletsListRepository.userWallets.map { requireNotNull(it) } + } + return userWalletsListManager.savedWalletsCount .filter { count -> if (count == 0) return@filter true diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 00451b45de..5b681fb679 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for getting selected wallet. @@ -15,10 +16,20 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetSelectedWalletSyncUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { + if (useNewRepository) { + return either { + userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) + } + } + return either { ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index d479a9d59b..57a01d23fb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -5,7 +5,9 @@ import arrow.core.raise.either import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull /** * Use case for getting flow of selected wallet. @@ -14,12 +16,32 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") +class GetSelectedWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { - userWalletsListManager.selectedUserWallet + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.filterNotNull() + } else { + userWalletsListManager.selectedUserWallet + } + } + } + + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") + fun sync(): Either { + return either { + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.value + } else { + userWalletsListManager.selectedUserWalletSync + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index b1a548af9a..6f4e13f0f0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -10,13 +10,24 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetUserWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWallets = userWalletsListManager.userWalletsSync + val userWallets = if (useNewListRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) @@ -25,7 +36,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa @OptIn(ExperimentalCoroutinesApi::class) fun invokeFlow(userWalletId: UserWalletId): EitherFlow { - return userWalletsListManager.userWallets.transformLatest { userWallets -> + val flow = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } + + return flow.transformLatest { userWallets -> userWallets.firstOrNull { it.walletId == userWalletId } ?.let { emit(it.right()) } ?: emit(GetUserWalletError.UserWalletNotFound.left()) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt index 0108e03b67..377bf1b152 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -1,13 +1,23 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for getting list of user wallets names. * * @property userWalletsListManager user wallets list manager */ -class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletNamesUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { - operator fun invoke(): List = userWalletsListManager.userWalletsSync.map { it.name } + operator fun invoke(): List = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name } + } else { + userWalletsListManager.userWalletsSync.map { it.name } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 7e6a0b6510..6635d63099 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -1,8 +1,10 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Use case for getting list of user wallets @@ -11,11 +13,23 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletsUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = userWalletsListManager.userWallets + operator fun invoke(): Flow> = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } @Throws(IllegalArgumentException::class) - fun invokeSync(): List = userWalletsListManager.userWalletsSync + fun invokeSync(): List = if (useNewListRepository) { + userWalletsListRepository.userWallets.value!! + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 29240ff71b..05d47b70d0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,12 +13,22 @@ import kotlinx.coroutines.flow.map * * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { +class IsNeedToBackupUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { operator fun invoke(id: UserWalletId): Flow { - return userWalletsListManager.userWallets + val userWalletsFlow = if (useNewRepository) { + userWalletsListRepository.userWallets + } else { + userWalletsListManager.userWallets + } + + return userWalletsFlow .map { wallets -> - val wallet = wallets.firstOrNull { it.walletId == id } + val wallet = wallets?.firstOrNull { it.walletId == id } if (wallet == null) { false } else { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 9ff8c5bb81..b34885eb98 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for saving user wallet @@ -18,22 +19,51 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class SaveWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - return either { - userWalletsListManager.save(userWallet, canOverride) - .doOnSuccess { return Unit.right() } - .doOnFailure { - return when (it) { - is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( - it.messageResId, - ) - else -> SaveWalletError.DataError(it.messageResId) - }.left() - } + return if (useNewRepository) { + either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() - return Unit.right() + if (newUserWallet) { + when (userWallet) { + is UserWallet.Cold -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } + is UserWallet.Hot -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.NoLock, + ) + } + }.mapLeft { SaveWalletError.DataError(null) }.bind() + } + } + } else { + either { + userWalletsListManager.save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { + return when (it) { + is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( + it.messageResId, + ) + else -> SaveWalletError.DataError(it.messageResId) + }.left() + } + + return Unit.right() + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 3ff5b201d7..e0654f947c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -6,9 +6,10 @@ import arrow.core.right import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for selecting wallet @@ -20,10 +21,19 @@ import com.tangem.domain.models.wallet.UserWalletId */ class SelectWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.select(userWalletId).map { + reduxStateHolder.onUserWalletSelected(it) + it + } + } + return either { return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b6d0accf29..96c2f19f7a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.wallets.models.UpdateWalletError.* +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for updating user wallet @@ -15,15 +18,38 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class UpdateWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either = either { - when (val result = userWalletsListManager.update(userWalletId, update)) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + ): Either { + if (useNewRepository) { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + ?: return Either.Left( + UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")), + ) + val updatedWallet = update(userWallet) + return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .mapLeft { + when (it) { + is SaveWalletError.DataError -> DataError( + IllegalStateException("Failed to update wallet: ${it.messageId}"), + ) + is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists + } + } + } + + return either { + when (val result = userWalletsListManager.update(userWalletId, update)) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt index b71be78c5e..4df283dd5e 100644 --- a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt @@ -22,7 +22,11 @@ class GetSavedWalletsCountUseCaseTest { @Before fun setup() { userWalletsListManager = mockk() - useCase = GetSavedWalletsCountUseCase(userWalletsListManager) + useCase = GetSavedWalletsCountUseCase( + userWalletsListManager, + userWalletsListRepository = mockk(), + useNewRepository = false, + ) mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") } diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 933716b04b..0713f99e5b 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -16,8 +16,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM import com.tangem.sdk.api.TangemSdkManager @@ -40,7 +40,7 @@ internal class AskBiometryModel @Inject constructor( private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, - private val userWalletsListManager: UserWalletsListManager, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val walletsRepository: WalletsRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, @@ -87,7 +87,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run { + val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { Timber.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 23b2dbb674..0c7b8d6290 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -51,6 +52,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, private val walletsRepository: WalletsRepository, @@ -231,7 +233,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, -> { - userWalletsListManager.save( + saveWalletUseCase( userWallet = userWalletCreated.copy( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), @@ -247,13 +249,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( } ?: userWalletCreated - userWalletsListManager.update( - userWalletId = userWallet.walletId, - update = { wallet -> - wallet.requireColdWallet().copy( - scanResponse = scanResponse.updateScanResponseAfterBackup(), - ) - }, + saveWalletUseCase( + userWallet = userWallet.requireColdWallet().copy( + scanResponse = scanResponse.updateScanResponseAfterBackup(), + ), + canOverride = true, ) userWallet diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 2e3a94788a..db44e189c2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -43,7 +43,8 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R @@ -73,7 +74,8 @@ internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, private val tangemSdkManager: TangemSdkManager, @@ -199,9 +201,9 @@ internal class OnboardingTwinModel @Inject constructor( // remove wallet only after first step of retwin if (params.mode == Mode.RecreateWallet) { - userWalletsListManager.delete( - listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), - ) + UserWalletIdBuilder.scanResponse(params.scanResponse).build()?.let { + deleteWalletUseCase(it) + } } analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) @@ -329,7 +331,14 @@ internal class OnboardingTwinModel @Inject constructor( return@coroutineScope } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@coroutineScope + } cardRepository.finishCardActivation(params.scanResponse.card.cardId) @@ -456,7 +465,15 @@ internal class OnboardingTwinModel @Inject constructor( return@launch } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@launch + } + params.modelCallbacks.onDone() } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 102098867b..e517a628cb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -20,8 +20,8 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent @@ -46,7 +46,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -173,7 +173,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } val userWallet = createUserWallet(params.scanResponse, newTokens) - userWalletsListManager.save(userWallet) + saveWalletUseCase(userWallet) visaAuthTokenStorage.remove(params.scanResponse.card.cardId) otpStorage.removeOTP(params.scanResponse.card.cardId) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index a23bc00f4b..743c26e5a2 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -25,12 +25,12 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError @@ -51,7 +51,7 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsStore: UserWalletsStore, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, @@ -409,7 +409,7 @@ internal class DefaultSwapRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - userWallet = requireNotNull(userWalletsListManager.selectedUserWalletSync), + userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 679015d755..118602742e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,8 +8,8 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -33,7 +33,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, + userWalletsStore: UserWalletsStore, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, @@ -43,7 +43,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsListManager = userWalletsListManager, + userWalletsStore = userWalletsStore, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index 1657e84545..3f365ebc8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -2,29 +2,44 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import timber.log.Timber class WalletNameMigrationUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, private val walletNamesMigrationRepository: WalletNamesMigrationRepository, ) { suspend operator fun invoke() { - val wallets = userWalletsListManager.userWalletsSync - if (walletNamesMigrationRepository.isMigrationDone()) { return } - val existingNames: MutableSet = mutableSetOf() - wallets.indices.forEach { i -> - val defaultName = wallets[i].name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + if (useNewListRepository) { + val wallets = userWalletsListRepository.userWalletsSync() + val existingNames: MutableSet = mutableSetOf() + wallets.forEach { + val defaultName = it.name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + } + Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + } + } else { + val wallets = userWalletsListManager.userWalletsSync + val existingNames: MutableSet = mutableSetOf() + wallets.indices.forEach { i -> + val defaultName = wallets[i].name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } - Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() From cf6b5dbabb26cdf91979ba10405b91f183180dde Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 19:05:20 +0400 Subject: [PATCH 104/165] Updated on 2026-08-14 --- .../common/SwitchEnvironmentInterceptor.kt | 13 +++++++++---- .../datasource/di/utils/RetrofitApiBuilder.kt | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index 1b2a53ec3c..ee59740bff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -15,12 +15,14 @@ import okio.IOException * Switch api environment [Interceptor] * * @property id api config id [ApiConfig.ID] + * @property baseUrls base urls for all api config environments * @property apiConfigsManager api configs manager * [REDACTED_AUTHOR] */ internal class SwitchEnvironmentInterceptor( private val id: ApiConfig.ID, + private val baseUrls: Set, private val apiConfigsManager: ApiConfigsManager, ) : Interceptor { @@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor( return chain.proceed(request) } - private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl { - return this.newBuilder() - .host(host = url.toHttpUrl().host) - .build() + private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl { + val currentUrl = this.toString() + val currentBaseUrl = baseUrls.first { currentUrl.contains(it) } + + return currentUrl + .replace(oldValue = currentBaseUrl, newValue = newBaseUrl) + .toHttpUrl() } private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt index 87de60fbd0..ee3a3cffd6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor @@ -42,6 +43,7 @@ import javax.inject.Singleton */ @Singleton internal class RetrofitApiBuilder @Inject constructor( + private val apiConfigs: ApiConfigs, private val apiConfigsManager: ApiConfigsManager, @NetworkMoshi private val moshi: Moshi, private val analyticsErrorHandler: AnalyticsErrorHandler, @@ -49,6 +51,8 @@ internal class RetrofitApiBuilder @Inject constructor( private val appLogsStore: AppLogsStore, ) { + private val configsBaseUrls: Map> = getConfigsBaseUrls() + /** * Builds a Retrofit API instance for the specified API configuration ID * @@ -95,13 +99,26 @@ internal class RetrofitApiBuilder @Inject constructor( val writeTimeoutSeconds: Long? = null, ) + private fun getConfigsBaseUrls(): Map> { + return apiConfigs.associate { config -> + val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl) + + config.id to allBaseUrls + } + } + private fun OkHttpClient.Builder.applyApiConfig( apiConfigId: ApiConfig.ID, environmentConfig: ApiEnvironmentConfig, ): OkHttpClient.Builder { return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { addInterceptor( - interceptor = SwitchEnvironmentInterceptor(id = apiConfigId, apiConfigsManager = apiConfigsManager), + interceptor = SwitchEnvironmentInterceptor( + id = apiConfigId, + baseUrls = configsBaseUrls[apiConfigId] + ?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"), + apiConfigsManager = apiConfigsManager, + ), ) } else { val headers = environmentConfig.headers From 853304539d7029383a5dc525169205018e08ee88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 16:23:18 +0400 Subject: [PATCH 105/165] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 13 ++-- core/res/src/main/res/values/strings.xml | 1 + .../DefaultAccountsCRUDRepository.kt | 6 ++ .../repository/AccountsCRUDRepository.kt | 9 ++- .../GetUnoccupiedAccountIndexUseCase.kt | 61 +++++++++++++++++ .../GetUnoccupiedAccountIndexUseCaseTest.kt | 59 +++++++++++++++++ features/account/impl/build.gradle.kts | 1 + .../createedit/AccountCreateEditModel.kt | 66 +++++++++++++++++-- .../createedit/entity/AccountCreateEditUM.kt | 14 +++- .../entity/AccountCreateEditUMBuilder.kt | 32 +++++++-- .../createedit/error/AccountFeatureError.kt | 30 +++++++++ .../createedit/ui/AccountCreateEditContent.kt | 21 +++--- 12 files changed, 285 insertions(+), 28 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 17e09eb709..8cf8edf8d6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,4 +41,12 @@ internal object AccountDomainModule { ): RecoverCryptoPortfolioUseCase { return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } + + @Provides + @Singleton + fun provideGetUnoccupiedAccountIndexUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): GetUnoccupiedAccountIndexUseCase { + return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..446bdcb138 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 723da17196..a46b5d096a 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -53,6 +53,12 @@ internal class DefaultAccountsCRUDRepository( } } + override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int { + val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1 + + return activeAccountsCount + 1 + } + override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a32796f0a1..a05f267633 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -43,15 +43,20 @@ interface AccountsCRUDRepository { * * @param accountList the list of accounts to be saved. */ - @Throws suspend fun saveAccounts(accountList: AccountList) + /** + * Retrieves the total count of accounts associated with a specific user wallet including archived accounts + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int + /** * Retrieves a user wallet by its unique identifier * * @param userWalletId the unique identifier of the user wallet * @return the [UserWallet] associated with the given identifier */ - @Throws fun getUserWallet(userWalletId: UserWalletId): UserWallet } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt new file mode 100644 index 0000000000..c34240e22b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for retrieving the next unoccupied account index + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetUnoccupiedAccountIndexUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Invokes the use case to calculate the next unoccupied account index + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId) + + DerivationIndex(totalAccountsCount + 1).getOrElse { + raise(Error.InvalidDerivationIndex(it)) + } + } + + private suspend fun Raise.getTotalAccountsCount(userWalletId: UserWalletId): Int { + return catch( + block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur in the use case + */ + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error" + + /** Error indicating that the derivation index is invalid */ + data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error { + override fun toString(): String = "$tag: Invalid derivation index: $cause" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt new file mode 100644 index 0000000000..ec8af7f2b7 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.account.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetUnoccupiedAccountIndexUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetUnoccupiedAccountIndexUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should return next unoccupied index when repository returns count`() = runTest { + // Arrange + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3 + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = 4.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } + + @Test + fun `invoke should return error if repository throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } +} \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index e68d20b144..dc77a38d28 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.core.error) implementation(projects.core.res) implementation(projects.core.decompose) implementation(projects.core.navigation) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index b4763b9c92..0f5a66dedf 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.account.createedit +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,11 +11,14 @@ import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.utils.showErrorDialog import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM @@ -21,15 +26,20 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateDerivationIndex import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName +import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped +@Suppress("LongParameterList") internal class AccountCreateEditModel @Inject constructor( paramsContainer: ParamsContainer, private val messageSender: UiMessageSender, @@ -37,13 +47,21 @@ internal class AccountCreateEditModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase, private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, + private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { private val params = paramsContainer.require() private val umBuilder = AccountCreateEditUMBuilder(params) - val uiState: StateFlow get() = _uiState - private val _uiState = MutableStateFlow(value = getInitialState()) + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + init { + if (params is AccountCreateEditComponent.Params.Create) { + updateDerivationInfo(userWalletId = params.userWalletId) + } + } private fun unsaveChangeDialog() { val secondAction = EventMessageAction( @@ -74,13 +92,16 @@ internal class AccountCreateEditModel @Inject constructor( private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { val state = uiState.value - val name = AccountName(state.account.name).getOrNull() ?: return + val name = AccountName(value = state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() + val index = state.account.derivationInfo.index ?: return + val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + addCryptoPortfolioUseCase( userWalletId = params.userWalletId, accountName = name, icon = icon, - derivationIndex = DerivationIndex.Main, // todo account + derivationIndex = derivationIndex, ) } @@ -100,19 +121,19 @@ internal class AccountCreateEditModel @Inject constructor( private fun onCloseClick() = unsaveChangeDialog() private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateIconSelect(icon) .validateNewState() } private fun onColorSelect(color: CryptoPortfolioIcon.Color) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateColorSelect(color) .validateNewState() } private fun onNameChange(name: String) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateName(name) .validateNewState() } @@ -140,4 +161,35 @@ internal class AccountCreateEditModel @Inject constructor( onCloseClick = ::onCloseClick, ) } + + private fun updateDerivationInfo(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.default) { + getUnoccupiedAccountIndexUseCase(userWalletId = userWalletId) + .onRight { derivationIndex -> + uiState.update { + it.updateDerivationIndex(derivationIndex = derivationIndex.value) + } + } + .onLeft { + handleError( + error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, + params = mapOf("userWalletId" to userWalletId.stringValue), + ) + + return@launch + } + } + } + + private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) + + Timber.e(exception) + + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent(exception = exception, params = params), + ) + + messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) + } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 4b133a4d97..df93dde4b9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -17,11 +17,23 @@ data class AccountCreateEditUM( data class Account( val name: String, val portfolioIcon: CryptoPortfolioIconUM, - val derivationInfo: TextReference, + val derivationInfo: DerivationInfo, val inputPlaceholder: TextReference, val onNameChange: (String) -> Unit, ) + sealed interface DerivationInfo { + val text: TextReference + val index: Int? + + data class Content(override val text: TextReference, override val index: Int) : DerivationInfo + + data object Empty : DerivationInfo { + override val text: TextReference = TextReference.EMPTY + override val index: Int? = null + } + } + data class Colors( val selected: CryptoPortfolioIcon.Color, val list: ImmutableList, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 41f12322bb..bacbd306ab 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -3,15 +3,15 @@ package com.tangem.features.account.createedit.entity import com.tangem.core.res.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -internal class AccountCreateEditUMBuilder @Inject constructor( - val params: AccountCreateEditComponent.Params, +internal class AccountCreateEditUMBuilder( + private val params: AccountCreateEditComponent.Params, ) { private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() @@ -29,14 +29,16 @@ internal class AccountCreateEditUMBuilder @Inject constructor( is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account( name = "", portfolioIcon = createIcon, - derivationInfo = TextReference.EMPTY, + derivationInfo = AccountCreateEditUM.DerivationInfo.Empty, inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), onNameChange = onNameChange, ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( name = params.account.name.value, portfolioIcon = params.account.portfolioIcon.toUM(), - derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ? + derivationInfo = createAccountDerivationInfo( + index = (params.account as Account.CryptoPortfolio).derivationIndex.value, + ), inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), onNameChange = onNameChange, ) @@ -113,5 +115,25 @@ internal class AccountCreateEditUMBuilder @Inject constructor( fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM { return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled)) } + + fun AccountCreateEditUM.updateDerivationIndex(derivationIndex: Int): AccountCreateEditUM { + return this.copy( + account = this.account.copy( + derivationInfo = createAccountDerivationInfo(index = derivationIndex), + ), + ) + } + + private fun createAccountDerivationInfo(index: Int): AccountCreateEditUM.DerivationInfo { + val derivationIndexText = if (index.toString().length == 1) "0$index" else "$index" + + return AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference( + id = R.string.account_form_account_index, + formatArgs = wrappedList(derivationIndexText), + ), + index = index, + ) + } } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt new file mode 100644 index 0000000000..9ab8549fc7 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt @@ -0,0 +1,30 @@ +package com.tangem.features.account.createedit.error + +import com.tangem.core.error.UniversalError + +sealed interface AccountFeatureError : UniversalError { + + val subsystemCode: String + val specificErrorCode: String + + override val errorCode: Int + get() = "108$subsystemCode$specificErrorCode".toInt() + + sealed interface CreateAccount : AccountFeatureError { + + override val subsystemCode: String get() = "001" + + data object UnableToGetDerivationIndex : CreateAccount { + override val specificErrorCode: String = "001" + } + } + + sealed interface EditAccount : AccountFeatureError { + + override val subsystemCode: String get() = "002" + + data object RequiredCryptoPortfolio : EditAccount { + override val specificErrorCode: String = "001" + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 2f98a553bc..b94e9198fe 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -32,10 +32,7 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.fields.AutoSizeTextField -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon @@ -76,7 +73,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi SpacerH8() Text( modifier = Modifier.padding(horizontal = 8.dp), - text = state.account.derivationInfo.resolveReference(), + text = state.account.derivationInfo.text.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -93,7 +90,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi } @Composable -private fun AccountSummary(account: AccountCreateEditUM.Account) { +private fun AccountSummary(account: Account) { Column( modifier = Modifier .clip(RoundedCornerShape(16.dp)) @@ -126,7 +123,7 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) { } @Composable -private fun AccountIcon(account: AccountCreateEditUM.Account) { +private fun AccountIcon(account: Account) { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -308,7 +305,10 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Date: Fri, 15 Aug 2025 16:29:40 +0500 Subject: [PATCH 106/165] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 82 ++++++++--- .../NavigationButtonsState.kt | 3 +- .../preview/NavigationButtonsPreview.kt | 36 +++-- .../features/send/v2/common/ui/SendContent.kt | 15 +- .../v2/common/ui/SendNavigationButtons.kt | 135 ------------------ .../features/send/v2/common/ui/SendingText.kt | 51 ------- .../v2/send/confirm/model/SendConfirmModel.kt | 3 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 2 +- .../success/ui/SendConfirmSuccessContent.kt | 30 ++-- .../confirm/model/NFTSendConfirmModel.kt | 3 +- .../confirm/ui/NFTSendConfirmContent.kt | 2 +- .../SetButtonsStateTransformer.kt | 44 +++--- .../confirm/model/SendWithSwapConfirmModel.kt | 3 + .../success/ui/SendWithSwapSuccessContent.kt | 70 +-------- .../sendviaswap/ui/SendWithSwapContent.kt | 32 ++--- 15 files changed, 155 insertions(+), 356 deletions(-) delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index b612670c59..5f2d7ca019 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -22,18 +24,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.buttons.common.contentColor import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty -import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.collections.immutable.ImmutableList +import com.tangem.core.ui.utils.singleEvent @Composable fun NavigationButtonsBlock( @@ -47,7 +48,7 @@ fun NavigationButtonsBlock( modifier = modifier.fillMaxWidth(), ) { InfoText(footerText) - ExtraButtons(state?.extraButtons, state?.txUrl) + DoneButtons(state?.extraButtons) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -58,9 +59,33 @@ fun NavigationButtonsBlock( } } +@Composable +fun NavigationButtonsBlockV2( + navigationUM: NavigationUM, + modifier: Modifier = Modifier, + footerText: TextReference? = null, +) { + val navigationUM = navigationUM as? NavigationUM.Content + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + InfoText(footerText) + DoneButtons(navigationUM?.secondaryPairButtonsUM) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + PreviousButton(navigationUM?.prevButton) + NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f)) + } + } +} + @Composable fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { val wrappedButton by rememberNavigationButton(primaryButton) + val hapticFeedback = LocalHapticFeedback.current AnimatedContent( targetState = wrappedButton, transitionSpec = { navigationButtonsTransition() }, @@ -83,7 +108,12 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier TangemButton( text = button.textReference.resolveReference(), enabled = button.isEnabled, - onClick = button.onClick, + onClick = { + if (button.isHapticClick) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + button.onClick() + }, showProgress = button.showProgress, colors = color, textStyle = TangemTheme.typography.subtitle1, @@ -123,33 +153,39 @@ private fun PreviousButton(prevButton: NavigationButton?) { } @Composable -private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: String?) { +fun DoneButtons(pairButtons: Pair?, modifier: Modifier = Modifier) { AnimatedVisibility( - visible = !txUrl.isNullOrBlank() && extraButtons != null, + visible = pairButtons != null, enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()), exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()), label = "Animate show sent state buttons", - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { - val buttons = remember(this) { requireNotNull(extraButtons) } + val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtons) } Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), ) { - buttons.forEach { button -> - val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) } - ?: TangemButtonIconPosition.None - TangemButton( - text = button.textReference.resolveReference(), - icon = icon, - textStyle = TangemTheme.typography.subtitle1, - onClick = rememberHapticFeedback(state = button, onAction = button.onClick), - modifier = Modifier.weight(1f), - enabled = button.isEnabled, - showProgress = false, - colors = TangemButtonsDefaults.secondaryButtonColors, - ) - } + SecondaryButtonIconStart( + text = leftButton.textReference.resolveReference(), + iconResId = requireNotNull(leftButton.iconRes), + onClick = { + singleEvent { + leftButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) + SecondaryButtonIconStart( + text = rightButton.textReference.resolveReference(), + iconResId = requireNotNull(rightButton.iconRes), + onClick = { + singleEvent { + rightButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) } } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index e0bddfab66..772c493cdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -2,7 +2,6 @@ package com.tangem.common.ui.navigationButtons import androidx.annotation.DrawableRes import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList sealed class NavigationButtonsState { data object Empty : NavigationButtonsState() @@ -10,7 +9,7 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton?, val prevButton: NavigationButton?, - val extraButtons: ImmutableList, + val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 9d2e71fe85..c409bb5b95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -5,29 +5,25 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import kotlinx.collections.immutable.persistentListOf internal object NavigationButtonsPreview { - private val extraButtons = persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), + private val extraButtons = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, ) private val prev = NavigationButton( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 289b609dff..0f860e75fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -5,9 +5,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.* +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.decompose.ComposableContentComponent @@ -45,7 +49,14 @@ internal fun SendContent( it.instance.Content(Modifier.weight(1f)) } if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) { - SendNavigationButtons(navigationUM = navigationUM) + NavigationButtonsBlockV2( + navigationUM = navigationUM, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt deleted file mode 100644 index 31cf103e3d..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.* -import androidx.compose.foundation.background -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.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.singleEvent - -@Composable -internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return - - Column( - modifier = modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - SendDoneButtons(navigationUM.secondaryPairButtonsUM) - SendNavigationButton( - navigationUM = navigationUM, - ) - } -} - -@Composable -private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - val navigationUM = navigationUM as? NavigationUM.Content ?: return - val primaryButton = navigationUM.primaryButton - - Row(modifier = modifier) { - AnimatedVisibility( - visible = navigationUM.prevButton != null, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) } - Row { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.button.secondary) - .clickableSingle(onClick = wrappedNavigationUM.onClick) - .padding(12.dp), - ) - SpacerW12() - } - } - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = { - if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - primaryButton.onClick() - }, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - ) - } -} - -@Composable -private fun SendDoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row(modifier = Modifier.padding(bottom = 12.dp)) { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = leftButton.iconRes!!, - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = rightButton.iconRes!!, - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt deleted file mode 100644 index da368be38b..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } - val keyboard by keyboardAsState() - - // the text should appear when the keyboard is closed - LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { - if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { - return@LaunchedEffect - } - isVisibleProxy = footerText != TextReference.EMPTY - } - - AnimatedVisibility( - visible = isVisibleProxy, - modifier = modifier, - enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), - exit = fadeOut(tween(durationMillis = 300)), - label = "Animate show sending state text", - ) { - Text( - text = footerText.resolveAnnotatedReference(), - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - ) - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index a458d480c5..1fb318f5c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -632,7 +632,8 @@ internal class SendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 73b2a5a0fa..7f7c70235b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference @@ -22,7 +23,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.FeeSelectorBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 862d4e1483..179bc3f11b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -1,17 +1,18 @@ package com.tangem.features.send.v2.send.success.ui import androidx.compose.animation.* +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.ui.AmountBlock -import com.tangem.core.ui.components.SpacerHMax +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -20,7 +21,6 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.SendNavigationButtons import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM @@ -50,7 +50,11 @@ internal fun SendConfirmSuccessContent( exit = slideOutVertically().plus(fadeOut()), label = "Animate success content", ) { - Column { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { Column( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) @@ -81,9 +85,19 @@ internal fun SendConfirmSuccessContent( ) destinationBlockComponent.Content(modifier = Modifier) feeBlockComponent.Content(modifier = Modifier) + Spacer(Modifier.height(60.dp)) } - SpacerHMax() - SendNavigationButtons(navigationUM = sendUM.navigationUM) + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index ae95518260..522470a90e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -428,7 +428,8 @@ internal class NFTSendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 9dbb04eb6d..c2a2de5774 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle @@ -20,7 +21,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 9c1ea68323..4c0166c007 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -13,8 +13,6 @@ import com.tangem.features.staking.impl.presentation.state.utils.getPendingActio import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal class SetButtonsStateTransformer( private val urlOpener: UrlOpener, @@ -23,12 +21,13 @@ internal class SetButtonsStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - extraButtons = getExtraButtons(prevState), - txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, + txUrl = txUrl, onTextClick = urlOpener::openUrl, ) } else { @@ -77,26 +76,23 @@ internal class SetButtonsStateTransformer( ).takeIf { prevState.currentStep.isPrevButtonVisible() } } - private fun getExtraButtons(prevState: StakingUiState): ImmutableList { - return persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_web_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onExploreClick, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_share_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onShareClick, - ), + private fun getExtraButtons(prevState: StakingUiState): Pair { + return NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onShareClick, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index a5aa42ce31..cebaaba9dd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -406,6 +406,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( it.second is SendWithSwapRoute.Confirm }.onEach { (state, _) -> val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isTransactionInProcess params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -416,6 +417,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( primaryButton = NavigationButton( textReference = resourceReference(R.string.common_send), iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isHapticClick = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, onClick = { when (confirmUM) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 2ad404315d..e77b8efd44 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.sendviaswap.success.ui import android.content.res.Configuration -import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -10,14 +9,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.transaction.Fee @@ -25,9 +19,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.InputRowBestRate @@ -41,7 +35,6 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters -import com.tangem.core.ui.utils.singleEvent import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType @@ -64,8 +57,6 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -private const val GRADIENT_ALPHA = 0.3f - @Composable internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { if (sendWithSwapUM.navigationUM !is NavigationUM.Content) return @@ -112,24 +103,15 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(60.dp)) } - DoneButtons( - pairButtonsUM = sendWithSwapUM.navigationUM.secondaryPairButtonsUM, + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendWithSwapUM.navigationUM, modifier = Modifier .align(Alignment.BottomCenter) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.tertiary.copy(GRADIENT_ALPHA), - TangemTheme.colors.background.tertiary, - ), - ), - ) .padding( - top = 24.dp, - bottom = 12.dp, start = 16.dp, end = 16.dp, + bottom = 16.dp, ), ) } @@ -289,46 +271,6 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m } } -// TODO remove [REDACTED_TASK_KEY] -@Composable -private fun DoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = requireNotNull(leftButton.iconRes), - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = requireNotNull(rightButton.iconRes), - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} - // region Preview @Suppress("LongMethod") @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index e9d03433d4..3075a24638 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -13,11 +13,9 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.plus import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -57,26 +55,14 @@ internal fun SendWithSwapContent( ) { it.instance.Content(Modifier.weight(1f)) } - // TODO refactor [REDACTED_TASK_KEY] - val primaryButton = navigationUM.primaryButton - Row( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - ) { - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = primaryButton.onClick, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, + if (stackState.active.configuration != SendWithSwapRoute.Success) { + NavigationPrimaryButton( + navigationUM.primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) } } From c2082a822c74b3f767e7ba49cf91e8fbb40da519 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 19:11:29 +0500 Subject: [PATCH 107/165] Updated on 2026-08-14 --- .../provider/ProviderChooseCrypto.kt | 1 - .../ui/extensions/ComposeNavigationExt.kt | 49 ------------- .../com/tangem/core/ui/extensions/Fragment.kt | 43 ----------- .../tangem/core/ui/extensions/ModifierExt.kt | 19 +++-- .../ui/screen/ComposeBottomSheetFragment.kt | 72 ------------------- .../tangem/core/ui/screen/ComposeFragment.kt | 50 ------------- .../ui/OnboardingVisaChooseWallet.kt | 20 +----- .../ui/FeeSelectorModalBottomSheet.kt | 3 +- .../ui/SwapChooseProviderBottomSheet.kt | 5 +- .../SwapChooseProviderContentPreview.kt | 6 +- 10 files changed, 16 insertions(+), 252 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index 0370272dcc..ace47189b0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -51,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { ConstraintLayout( modifier = modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = providerChooseUM.isSelected) .clickable( enabled = !providerChooseUM.hasError(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt deleted file mode 100644 index 2dc5bcd17e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.core.ui.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import timber.log.Timber - -/** - * The ViewModel is scoped to the parent route Navigation graph - * and is provided using the Hilt-generated ViewModel factory - * - * ``` - * val navController = rememberNavController() - * - * navigation( - * route = "parent", - * startDestination = "parent/1" - * ) { - * composable("route/1") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/2") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/3") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * } - * ``` - * - * @param navController NavController within the common NavGraph - * @throws Exception if there is no parent route - */ -@Composable -inline fun NavBackStackEntry.parentHiltViewModel(navController: NavController): T { - val viewModelStoreOwner = remember(this) { - try { - navController.getBackStackEntry(this.destination.parent!!.id) - } catch (e: Exception) { - Timber.tag("scopedViewModel").e(e, "There is no parent route'") - throw e - } - } - - return hiltViewModel(viewModelStoreOwner) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt deleted file mode 100644 index e58126708e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.core.ui.extensions - -import android.R -import android.content.Context -import android.graphics.Color.* -import android.view.WindowManager -import androidx.annotation.ColorRes -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment -import kotlin.math.sqrt - -@Deprecated("Use only in legacy fragments") -fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { - with(requireActivity().window) { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - statusBarColor = ContextCompat.getColor(requireContext(), colorResId) - val view = view ?: return - val windowInsetsController = WindowCompat.getInsetsController(this, view) - windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId) - } -} - -// TODO replace by android.graphics.luminance() after bump min API to 24 -@Suppress("MagicNumber") -fun luminance(context: Context, @ColorRes colorRes: Int): Boolean { - val color = context.resources.getColor(colorRes, null) - if (R.color.transparent == color) return true - var rtnValue = false - val rgb = intArrayOf(red(color), green(color), blue(color)) - val brightness = sqrt( - rgb[0] * rgb[0] * .241 + - rgb[1] * rgb[1] * .691 + - rgb[2] * rgb[2] * .068, - ).toInt() - - // color is light - if (brightness >= 200) { - rtnValue = true - } - return rtnValue -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index ef94c6b681..cab83ed0d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -72,26 +71,24 @@ fun Modifier.conditionalCompose( fun Modifier.selectedBorder( isSelected: Boolean, width: Dp = 2.5.dp, - color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + color: Color = TangemTheme.colors.text.accent, radius: Dp = 16.dp, ) = conditionalCompose( condition = isSelected, modifier = { - border( + outsetBorder( width = width, - color = color, - shape = RoundedCornerShape(radius), + color = color.copy(alpha = 0.15f), + shape = RoundedCornerShape(radius + 2.dp), ) - .padding(width) .border( width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(radius - 2.dp), + color = color, + shape = RoundedCornerShape(radius), ) - .clip(RoundedCornerShape(radius - 2.dp)) + .clip(RoundedCornerShape(radius)) }, otherModifier = { - padding(width) - .clip(RoundedCornerShape(radius - 2.dp)) + clip(RoundedCornerShape(radius)) }, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt deleted file mode 100644 index c5c4285292..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.screen - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.FloatRange -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Modifier -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme - -/** - * An abstract base class for bottom sheet dialogs that use Compose for UI rendering. - * Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { - - /** - * The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED]. - */ - open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED - - /** - * The fraction of the screen height that the bottom sheet should take when expanded. - * Default is `null`, indicating that the height will be determined by the content. - */ - @FloatRange(from = 0.0, to = 1.0) - open val expandedHeightFraction: Float? = null - - override val screenModifier: Modifier - @Composable - @ReadOnlyComposable - get() = Modifier - .fillMaxWidth() - .let { - if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it - } - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ) - - override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView( - context = inflater.context, - activity = requireActivity(), - overrideSystemBarColors = false, - ) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - - (dialog as BottomSheetDialog).behavior.apply { - state = initialBottomSheetState - skipCollapsed = true - } - - return dialog - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt deleted file mode 100644 index 48565c82d7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.core.ui.screen - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import com.tangem.core.ui.R - -/** - * An abstract base class for fragments that use Compose for UI rendering. - * Extends [Fragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeFragment : Fragment(), ComposeScreen { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() - - return createComposeView(inflater.context, requireActivity()).also { - it.isTransitionGroup = isTransitionsInflated - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually dispatch configuration changes to the Compose view. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - view?.dispatchConfigurationChanged(newConfig) - } - - /** - * Inflates transitions for the fragment. Override this method to customize - * enter and exit transitions for the fragment. - * - * @return `true` if transitions were inflated; `false` otherwise. - */ - protected open fun TransitionInflater.inflateTransitions(): Boolean { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) - - return true - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index 5cd9a11f10..dff0ca06c5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -1,15 +1,11 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -19,9 +15,9 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -112,17 +108,7 @@ private fun SelectableChainRow( RowContentContainer( modifier = modifier .heightIn(min = 48.dp) - .outsetBorder( - color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent, - width = 5.dp, - shape = RoundedCornerShape(size = 18.dp), - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .border( - width = 1.dp, - color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) + .selectedBorder(selected) .clickable(onClick = onClick) .padding(12.dp), icon = { @@ -163,7 +149,7 @@ private fun Preview() { ), ), selectedOption = SelectableChainRowUM( - event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet, icon = R.drawable.ic_tangem_24, text = TextReference.Str("Tangem Wallet"), ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 5b1406dd0b..8f3ef2cb54 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -79,7 +79,7 @@ internal fun FeeSelectorModalBottomSheet( FeeSelectorItems( state = state, feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp), + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, footer = { @@ -141,7 +141,6 @@ private fun FeeSelectorItems( ) val itemModifier = Modifier .fillMaxWidth() - .background(TangemTheme.colors.background.primary) .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 9772ead6c8..77f8f0cbef 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -5,14 +5,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -65,7 +63,7 @@ internal fun SwapChooseProviderContent( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(horizontal = 13.dp), + modifier = modifier.padding(horizontal = 12.dp), ) { Text( text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), @@ -89,7 +87,6 @@ internal fun SwapChooseProviderContent( SwapProviderItem( state = provider.swapProviderState, modifier = Modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = provider.swapProviderState.isSelected) .clickable( enabled = provider.quote !is SwapQuoteUM.Error, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 0975e02bdf..bf7a8ef284 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -96,13 +96,13 @@ internal object SwapChooseProviderContentPreview { ), quote = quote2, swapProviderState = SwapProviderState.Content( - name = provider1.name, - type = provider1.type.typeName, + name = provider2.name, + type = provider2.type.typeName, iconUrl = "", subtitle = stringReference("1800 POL"), additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, - isSelected = true, + isSelected = false, ), ), ), From dbbbdd2d33e99a38aa0dbba0358d13c4dba35a51 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:27:38 +0500 Subject: [PATCH 108/165] Updated on 2026-08-14 --- .../DefaultFeeSelectorBlockComponent.kt | 5 +++- .../feeselector/ui/FeeSelectorBlockContent.kt | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 7bfbe898e3..5cb0f113e0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -18,6 +18,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent +import com.tangem.utils.extensions.isSingleItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -73,11 +74,13 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() + val isScreenSource = params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen + val isNotSingleFee = (state as? FeeSelectorUM.Content)?.feeItems?.isSingleItem() == false FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, modifier = modifier - .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { + .conditional(isScreenSource && isNotSingleFee) { Modifier.clickable { model.showFeeSelector() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index b0f08978aa..2f403f5b0c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -39,6 +39,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.impl.R +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -118,12 +119,11 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie text = annotatedString, modifier = Modifier .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), + .size(TangemTheme.dimens.size16) + .clip(CircleShape), content = { contentModifier -> Icon( - modifier = contentModifier - .size(TangemTheme.dimens.size16) - .clip(CircleShape), + modifier = contentModifier.size(TangemTheme.dimens.size16), painter = painterResource(id = R.drawable.ic_token_info_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -176,12 +176,14 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi textAlign = TextAlign.End, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + if (!state.feeItems.isSingleItem()) { + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } } } @@ -220,7 +222,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider { ), FeeSelectorUM.Content( isPrimaryButtonEnabled = false, - feeItems = persistentListOf(maxFeeItem), + feeItems = persistentListOf(lowFeeItem, maxFeeItem), selectedFeeItem = maxFeeItem, feeExtraInfo = FeeExtraInfo( isFeeApproximate = false, From d6f8549bd26eda1920985a5714eb7c996153ff8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:27:45 +0500 Subject: [PATCH 109/165] Updated on 2026-08-14 --- .../model/converter/SwapQuoteUMConverter.kt | 2 + .../SwapAmountSetQuotesTransformer.kt | 24 +++- .../impl/amount/ui/SwapAmountBlockContent.kt | 125 ++++++++++++------ .../ui/preview/SwapAmountContentPreview.kt | 1 + .../ui/SwapChooseProviderContent.kt | 11 +- .../SwapChooseProviderContentPreview.kt | 2 + .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 1 + 7 files changed, 118 insertions(+), 48 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index f5738cbd5e..c014e539e7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -57,6 +57,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } else { @@ -68,6 +69,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index b9d220ba0b..adfcc37a91 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.Differ import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -26,12 +27,20 @@ internal class SwapAmountSetQuotesTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val isSingleProvider = quotes.filter { + it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { prevState.selectedQuote } else { - (bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote + (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( @@ -47,16 +56,23 @@ internal class SwapAmountSetQuotesTransformer( return updatedState.copy( isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), + swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), ) } - private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { + private fun getQuotesWithDiff( + sortedQuotes: List, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy(diffPercent = DifferencePercent.Best) + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { // current / selected - 1 val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index c68f7e3923..f7129e04bc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.extensions.TextReference @@ -34,6 +36,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -63,33 +66,11 @@ internal fun SwapAmountBlockContent( ), ) { val (from, to, separator, provider) = createRefs() - AmountBlockV2( - amountState = amountUM.primaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(from) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) }, - ) - AmountBlockV2( - amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( - title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, - availableBalanceShort = TextReference.EMPTY, - ) ?: amountUM.secondaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(to) { - top.linkTo(from.bottom, 8.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick) - }, + SwapAmountBlock( + amountUM = amountUM, + fromAmountRef = from, + toAmountRef = to, + onInfoClick = onInfoClick, ) SwapAmountDivider( modifier = Modifier.constrainAs(separator) { @@ -103,6 +84,7 @@ internal fun SwapAmountBlockContent( val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( isBestRate = isBestRate, + isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.showBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, @@ -119,29 +101,88 @@ internal fun SwapAmountBlockContent( } @Composable -private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) { +private fun ConstraintLayoutScope.SwapAmountBlock( + amountUM: SwapAmountUM.Content, + fromAmountRef: ConstrainedLayoutReference, + toAmountRef: ConstrainedLayoutReference, + onInfoClick: () -> Unit, +) { + AmountBlockV2( + amountState = amountUM.primaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(fromAmountRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) + AmountBlockV2( + amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( + title = resourceReference(R.string.send_with_swap_recipient_amount_title), + availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, + ) ?: amountUM.secondaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(toAmountRef) { + top.linkTo(fromAmountRef.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) +} + +@Composable +private fun SwapPriceImpact( + amountFieldUM: SwapAmountFieldUM, + selectedAmountType: SwapAmountType, + onInfoClick: () -> Unit, +) { + if (amountFieldUM.amountType == selectedAmountType) return + val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact + val iconColor = if (priceImpact != null) { + TangemTheme.colors.icon.attention + } else { + TangemTheme.colors.icon.informative + } + if (priceImpact != null) { Text( text = priceImpact.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.attention, ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_information_24), - ), - tint = TangemTheme.colors.icon.attention, - contentDescription = null, - modifier = Modifier - .size(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = onInfoClick, - ), - ) } + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_information_24), + ), + tint = iconColor, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onInfoClick, + ), + ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 627af48d84..02419e0cd6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -69,6 +69,7 @@ internal data object SwapAmountContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) val emptyState = SwapAmountUM.Content( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 6751315374..a853552187 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.delay @Composable fun SwapChooseProviderContent( expressProvider: ExpressProvider?, + isSingleProvider: Boolean, isBestRate: Boolean, showBestRateAnimation: Boolean, onClick: () -> Unit, @@ -62,6 +63,7 @@ fun SwapChooseProviderContent( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, + enabled = !isSingleProvider, ), ) { HorizontalDivider( @@ -88,6 +90,7 @@ fun SwapChooseProviderContent( ProviderInfo( expressProvider = expressProvider, isBestRate = isBestRate, + isSingleProvider = isSingleProvider, showBestRateAnimation = showBestRateAnimation, onFinishAnimation = onFinishAnimation, ) @@ -125,6 +128,7 @@ private fun FcaProviderWarning(modifier: Modifier = Modifier) { private fun ProviderInfo( expressProvider: ExpressProvider?, isBestRate: Boolean, + isSingleProvider: Boolean, showBestRateAnimation: Boolean, onFinishAnimation: () -> Unit, modifier: Modifier = Modifier, @@ -166,6 +170,7 @@ private fun ProviderInfo( start.linkTo(imageRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) + end.linkTo(iconRef.start, goneMargin = 12.dp) }, ) Icon( @@ -180,12 +185,13 @@ private fun ProviderInfo( start.linkTo(nameRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) - end.linkTo(parent.end, 12.dp) + end.linkTo(parent.end, margin = 12.dp) + visibility = if (isSingleProvider) Visibility.Gone else Visibility.Visible }, ) BestRateBadge( showBestRateAnimation = showBestRateAnimation, - isBestRate = isBestRate, + isBestRate = isBestRate && !isSingleProvider, ref = imageRef, onFinishAnimation = onFinishAnimation, ) @@ -325,6 +331,7 @@ private fun SwapChooseProviderContent_Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) { SwapChooseProviderContent( + isSingleProvider = false, isBestRate = true, showBestRateAnimation = true, expressProvider = ExpressProvider( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 0975e02bdf..f1b3d9f609 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -39,6 +39,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) private val quote2 = SwapQuoteUM.Content( @@ -47,6 +48,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("13.12"), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSingleProvider = false, ) val state = SwapChooseProviderBottomSheetContent( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index ff661ba3ee..c8b7e6f038 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -34,6 +34,7 @@ internal sealed class SwapQuoteUM { val quoteAmount: BigDecimal, val quoteAmountValue: TextReference, val diffPercent: DifferencePercent, + val isSingleProvider: Boolean, val rate: TextReference, ) : SwapQuoteUM() { sealed class DifferencePercent { From 48b8c77f5d19b5833278cfba63a61b9058c8e09c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 14:19:48 +0500 Subject: [PATCH 110/165] Updated on 2026-08-14 --- .../main/res/drawable/ic_passcode_lock_32.xml | 13 +++ .../main/res/drawable/ic_passcode_lock_56.xml | 20 +++++ .../port/entity/AddExistingWalletImportUM.kt | 2 - .../model/AddExistingWalletImportModel.kt | 33 +++++++ .../model/ImportSeedPhraseUiStateBuilder.kt | 18 +--- .../port/ui/AddExistingWalletImportContent.kt | 4 - .../im/port/ui/PassphraseInfoBottomSheet.kt | 90 ------------------- .../walletbackup/entity/WalletBackupUM.kt | 1 + .../walletbackup/model/WalletBackupModel.kt | 40 ++++++++- .../walletbackup/ui/WalletBackupContent.kt | 3 + 10 files changed, 111 insertions(+), 113 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_passcode_lock_32.xml create mode 100644 core/ui/src/main/res/drawable/ic_passcode_lock_56.xml delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml new file mode 100644 index 0000000000..b610863fbc --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml new file mode 100644 index 0000000000..ecf6f9754c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml @@ -0,0 +1,20 @@ + + + + diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt index 667d538a14..efd6c04702 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.entity import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -18,6 +17,5 @@ internal data class AddExistingWalletImportUM( val importWalletClick: () -> Unit, val suggestionsList: ImmutableList, val onSuggestionClick: (String) -> Unit, - val infoBottomSheetConfig: TangemBottomSheetConfig, val readyToImport: Boolean, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index f11d60b7bc..29196737e8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,8 +1,18 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -19,6 +29,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletImportModel @Inject constructor( paramsContainer: ParamsContainer, @@ -27,12 +38,29 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder + private val passphraseInfoAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_56) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.common_passphrase) + body = resourceReference(R.string.onboarding_bottom_sheet_passphrase_description) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + init { importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, @@ -45,6 +73,7 @@ internal class AddExistingWalletImportModel @Inject constructor( passphrase = passphrase, ) }, + onPassphraseInfoClick = ::onPassphraseInfoClick, ) } @@ -73,4 +102,8 @@ internal class AddExistingWalletImportModel @Inject constructor( } } } + + private fun onPassphraseInfoClick() { + uiMessageSender.send(passphraseInfoAlertBS) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index b09bdd3265..7f89025442 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult @@ -24,6 +23,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val readyToImport: (Boolean) -> Unit, private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, + private val onPassphraseInfoClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder( passphrase = it.text updateUiState { state -> state.copy(passPhrase = it) } }, - onPassphraseInfoClick = ::showInfoBS, + onPassphraseInfoClick = onPassphraseInfoClick, importWalletClick = ::onCreateWallet, onSuggestionClick = { word -> addSuggestedWord(word) }, readyToImport = false, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, ) } @@ -168,19 +167,6 @@ internal class ImportSeedPhraseUiStateBuilder( } } - private fun showInfoBS() { - updateUiState { state -> - state.copy( - infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy( - isShown = true, - onDismissRequest = { - updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) } - }, - ), - ) - } - } - companion object { private const val MINIMUM_WORD_LENGTH = 2 private const val WORDS_INTERCEPT_DELAY_MS = 500L diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 9d1d73601c..d7bb9338b0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -37,7 +37,6 @@ import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.OutlineTextFieldWithIcon import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation @@ -113,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } } - - PassphraseInfoBottomSheet(state.infoBottomSheetConfig) } @Composable @@ -233,7 +230,6 @@ private fun PreviewAddExistingWalletImportContent() { importWalletClick = {}, suggestionsList = persistentListOf(), onSuggestionClick = {}, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, readyToImport = false, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt deleted file mode 100644 index 76521aab7a..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.im.port.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, - ) { _: TangemBottomSheetConfigContent.Empty -> - PassphraseInfoBottomSheetContent(config.onDismissRequest) - } -} - -@Composable -fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = TangemTheme.dimens.size40) - .size(TangemTheme.dimens.size48), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - - Text( - text = stringResourceSafe(id = R.string.common_passphrase), - modifier = Modifier - .padding(top = TangemTheme.dimens.size40) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - - Text( - text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), - modifier = Modifier - .padding(top = TangemTheme.dimens.size16) - .padding(horizontal = TangemTheme.dimens.size24) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - ) - - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding(top = TangemTheme.dimens.size40) - .padding(bottom = TangemTheme.dimens.size32) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_ok), - onClick = onDismiss, - ) - } -} - -@Preview -@Composable -private fun PassphraseInfoBottomSheetContentPreview() { - TangemThemePreview { - PassphraseInfoBottomSheetContent({ }) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index 5df45d62a0..f3f17cd9d3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -8,6 +8,7 @@ internal data class WalletBackupUM( val googleDriveStatus: LabelUM?, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, + val backedUp: Boolean, ) internal sealed class BackupStatus { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 18e601c406..37c2dd7948 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -1,13 +1,21 @@ package com.tangem.features.hotwallet.walletbackup.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.WalletBackupComponent @@ -22,6 +30,7 @@ internal class WalletBackupModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -38,11 +47,31 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), - onRecoveryPhraseClick = { }, + onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, + backedUp = false, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + closeBs() + // TODO [REDACTED_TASK_KEY] + } + } + } + init { getWalletUseCase.invokeFlow(params.userWalletId) .map { it.getOrNull() } @@ -80,5 +109,14 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + backedUp = userWallet.backedUp, ) + + private fun onRecoveryPhraseClick() { + if (uiState.value.backedUp) { + // TODO [REDACTED_TASK_KEY] + } else { + uiMessageSender.send(makeBackupAtFirstAlertBS) + } + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 66e0c92d43..f5d25b7280 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -96,6 +96,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider Date: Mon, 18 Aug 2025 12:34:18 +0300 Subject: [PATCH 111/165] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../DefaultUserWalletsListRepository.kt | 4 +- .../tangem/tap/routing/utils/ChildFactory.kt | 31 ++- .../common/ui/userwallet/UserWalletItem.kt | 28 +++ .../converter/UserWalletItemUMConverter.kt | 41 ++-- .../ui/userwallet/state/UserWalletItemUM.kt | 2 + .../res/drawable/ic_mobile_wallet_icon_24.xml | 19 ++ .../walletmanager/WalletManagerFactory.kt | 3 +- .../data/wallets/hot/HotWalletAccessor.kt | 5 +- .../wallets/builder/HotUserWalletBuilder.kt | 2 +- .../usecase/GetIsBiometricsEnabledUseCase.kt | 4 + .../wallets/usecase/SaveWalletUseCase.kt | 6 +- .../details/model/UserWalletListModel.kt | 1 + .../hotwallet/accesscode/AccessCodeModel.kt | 46 +++- .../CreateMobileWalletModel.kt | 5 +- .../wallet/utils/UserWalletsFetcher.kt | 1 + .../wallet/utils/DefaultUserWalletsFetcher.kt | 12 +- .../connections/utils/WcUserWalletsFetcher.kt | 1 + features/welcome/impl/build.gradle.kts | 4 + .../welcome/impl/model/WelcomeModel.kt | 217 +++++++++++++++++- .../features/welcome/impl/ui/Welcome.kt | 41 ++-- .../welcome/impl/ui/WelcomeEnterAccessCode.kt | 94 -------- .../features/welcome/impl/ui/WelcomePlain.kt | 12 + .../welcome/impl/ui/WelcomeSelectWallet.kt | 79 +------ .../welcome/impl/ui/state/WalletUM.kt | 22 -- .../welcome/impl/ui/state/WelcomeUM.kt | 10 +- tangem-android-tools | 2 +- 27 files changed, 432 insertions(+), 262 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 23aae9e349..3ac868e93f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193 +Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index c1cb92a998..6d907c384b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -77,7 +77,9 @@ internal class DefaultUserWalletsListRepository( override suspend fun userWalletsSync(): List { load() - return userWallets.value!! + return requireNotNull(userWallets.value) { + "This should never happen" + } } override suspend fun selectedUserWalletSync(): UserWallet? { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 49f217bf18..71f7c77d8b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -19,6 +19,7 @@ import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -52,6 +53,7 @@ import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent +import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -70,6 +72,7 @@ internal class ChildFactory @Inject constructor( private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val welcomeComponentFactory: WelcomeComponent.Factory, + private val newWelcomeComponentFactory: NewWelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, private val stakingComponentFactory: StakingComponent.Factory, private val swapComponentFactory: SwapComponent.Factory, @@ -102,6 +105,7 @@ internal class ChildFactory @Inject constructor( private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -138,14 +142,25 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Welcome -> { - createComponentChild( - context = context, - params = WelcomeComponent.Params( - launchMode = route.launchMode, - intent = route.intent, - ), - componentFactory = welcomeComponentFactory, - ) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + createComponentChild( + context = context, + params = NewWelcomeComponent.Params( + launchMode = route.launchMode, + intent = route.intent, + ), + componentFactory = newWelcomeComponentFactory, + ) + } else { + createComponentChild( + context = context, + params = WelcomeComponent.Params( + launchMode = route.launchMode, + intent = route.intent, + ), + componentFactory = welcomeComponentFactory, + ) + } } is AppRoute.WalletSettings -> { createComponentChild( diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 97a5063ec8..31c66358b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -3,7 +3,9 @@ package com.tangem.common.ui.userwallet import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -22,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.common.ui.R @@ -184,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi radius = TangemTheme.dimens.size2, ) } + is UserWalletItemUM.ImageState.MobileWallet -> { + Image( + modifier = Modifier + .size(36.dp) + .background( + color = TangemTheme.colors.field.focused, + shape = RoundedCornerShape(10.dp), + ) + .padding(6.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24), + contentDescription = null, + ) + } is UserWalletItemUM.ImageState.Image -> { val verifiedArtwork = imageState.artwork.verifiedArtwork if (verifiedArtwork != null) { @@ -364,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { @@ -48,24 +49,38 @@ class UserWalletItemUMConverter( name = stringReference(name), information = getInfo(userWallet = this), balance = getBalanceInfo(userWallet = this), - isEnabled = !isLocked, + isEnabled = isEnabled(userWallet = this), endIcon = endIcon, onClick = { onClick(value.walletId) }, - imageState = artwork?.let { - UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it)) - } ?: UserWalletItemUM.ImageState.Loading, - label = if (this is UserWallet.Hot && !this.backedUp) { - LabelUM( - text = resourceReference(R.string.hw_backup_no_backup), - style = LabelStyle.WARNING, - ) - } else { - null - }, + imageState = getImageState(userWallet = value), + label = getLabelOrNull(userWallet = this), ) } } + private fun isEnabled(userWallet: UserWallet): Boolean { + return authMode || userWallet.isLocked.not() + } + + private fun getLabelOrNull(userWallet: UserWallet): LabelUM? { + return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + } else { + null + } + } + + private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState { + return when { + userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet + artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork)) + else -> UserWalletItemUM.ImageState.Loading + } + } + private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index e9f98863b8..c0abb2b485 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -56,6 +56,8 @@ data class UserWalletItemUM( data object Loading : ImageState() + data object MobileWallet : ImageState() + data class Image( val artwork: ArtworkUM, ) : ImageState() diff --git a/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml new file mode 100644 index 0000000000..101239eb7d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index eff1e685f6..2ab20f0a42 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp +import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -41,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = blockchain.getSupportedCurves().first() + val curve = Wallet2CardConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index c7569b5a3b..9c26e8a3ee 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -46,9 +46,8 @@ class HotWalletAccessor @Inject constructor( auth = auth, block = { blockAuth -> block(blockAuth).also { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code - // if user has biometry enabled, we set it as the new auth method - if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { + // Update biometry auth if the original auth was password + if (blockAuth is HotAuth.Password) { tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( walletId = hotWalletId, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index 4707a09f81..eaa1c7dc7c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt index b57e8d3ec8..2c3f0deaba 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt @@ -8,4 +8,8 @@ class GetIsBiometricsEnabledUseCase @Inject constructor( ) { operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false + + fun canUseBiometry(): Boolean { + return tangemSdkManager.canUseBiometry + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index b34885eb98..23b10bf3c9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -46,7 +46,11 @@ class SaveWalletUseCase( UserWalletsListRepository.LockMethod.NoLock, ) } - }.mapLeft { SaveWalletError.DataError(null) }.bind() + }.mapLeft { + SaveWalletError.DataError(null) + }.map { + userWalletsListRepository.select(userWallet.walletId) + }.bind() } } } else { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 6af5b54fe3..d1d008e54f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -36,6 +36,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, + authMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index a7e9eec930..373af1cf05 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -5,10 +5,10 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth @@ -27,7 +27,7 @@ internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, - private val saveWalletUseCase: SaveWalletUseCase, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemHotSdk: TangemHotSdk, ) : Model() { @@ -77,15 +77,41 @@ internal class AccessCodeModel @Inject constructor( runCatching { val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet with id $userWalletId not found") } - if (userWallet is UserWallet.Hot) { - val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) - val updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, + if (userWallet !is UserWallet.Hot) return@launch + + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, auth = HotAuth.Password(accessCode.toCharArray()), - ) - saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true) - params.callbacks.onAccessCodeConfirmed(params.userWalletId) - } + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet.copy( + hotWalletId = updatedHotWalletId, + backedUp = true, + ), + canOverride = true, + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + + params.callbacks.onAccessCodeConfirmed(params.userWalletId) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index d59cf2e39f..0b3261b4f3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -45,9 +45,8 @@ internal class CreateMobileWalletModel @Inject constructor( runCatching { val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) - saveUserWalletUseCase( - hotUserWalletBuilder.build(), - ) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase(userWallet) router.replaceAll(AppRoute.Wallet) }.onFailure { Timber.e(it) diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 3f30b7eebe..674541796f 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -14,6 +14,7 @@ interface UserWalletsFetcher { fun create( messageSender: UiMessageSender, onlyMultiCurrency: Boolean, + authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): UserWalletsFetcher } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index ce71cf24a6..7a3b9accba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -43,7 +43,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, - @Assisted private val onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, + @Assisted("authMode") private val authMode: Boolean, private val getCardImageUseCase: GetCardImageUseCase, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { @@ -54,7 +55,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> - val uiModels = UserWalletItemUMConverter(onClick = onWalletClick).convertList(wallets) + val uiModels = UserWalletItemUMConverter( + onClick = onWalletClick, + authMode = authMode, + ).convertList(wallets) .toImmutableList() emit(uiModels) @@ -132,6 +136,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], + authMode = authMode, ) .convert(userWallet) } @@ -149,7 +154,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( interface Factory : UserWalletsFetcher.Factory { override fun create( messageSender: UiMessageSender, - onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean, + @Assisted("authMode") authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index bcfd5129b1..727e85c485 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -29,6 +29,7 @@ internal class WcUserWalletsFetcher( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = true, + authMode = false, onWalletClick = { onWalletSelected(it) }, ) diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 499c0ab82e..8a2a3c3c3f 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -13,11 +13,13 @@ android { dependencies { implementation(projects.features.welcome.api) + implementation(projects.features.wallet.api) /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.analytics) implementation(projects.common.routing) implementation(projects.common.ui) @@ -30,6 +32,7 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.wallets) + implementation(projects.domain.card) /** DI */ implementation(deps.hilt.android) @@ -54,4 +57,5 @@ dependencies { implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index da1f8c9c21..b81a8806c5 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -1,18 +1,233 @@ package com.tangem.features.welcome.impl.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.features.welcome.impl.R +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.* import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val uiMessageSender: UiMessageSender, + private val userWalletsFetcherFactory: UserWalletsFetcher.Factory, + private val userWalletsListRepository: UserWalletsListRepository, + private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, ) : Model() { + // TODO add intent handling + // val params val uiState: StateFlow - field = MutableStateFlow(WelcomeUM.Plain) + field = MutableStateFlow(WelcomeUM.Plain) + + private val walletsFetcher = userWalletsFetcherFactory.create( + messageSender = uiMessageSender, + onlyMultiCurrency = false, + authMode = true, + onWalletClick = { walletId -> + modelScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first { it.walletId == walletId } + onUserWalletClick(userWallet) + } + }, + ) + private val walletsFetcherJobHolder = JobHolder() + private val wallets = MutableStateFlow>(persistentListOf()) + + init { + modelScope.launch { + userWalletsListRepository.load() + wallets.value = walletsFetcher.userWallets.first() + + launch { + walletsFetcher.userWallets + .collectLatest { wallets.value = it } + } + + tryToUnlockRightAway() + } + } + + private fun tryToUnlockRightAway() { + modelScope.launch { + if (canUnlockWithBiometrics()) { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }) + setSelectWalletState() + } + } else { + tryToUnlockWithAccessCodeRightAway() + setSelectWalletState() + } + } + } + + private fun tryToUnlockWithAccessCodeRightAway() = modelScope.launch { + if (onlyOneHotWalletWithAccessCode()) { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first() + unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) + } + } + + private fun setSelectWalletState() { + modelScope.launch { + uiState.value = WelcomeUM.SelectWallet( + wallets = walletsFetcher.userWallets.first(), + showUnlockWithBiometricButton = canUnlockWithBiometrics(), + addWalletClick = ::addWalletClick, + onUnlockWithBiometricClick = { + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { /* ignore */ }) + } + } + }, + ) + + wallets.collectLatest { wallets -> + updateSelectState { + it.copy(wallets = wallets) + } + } + }.saveIn(walletsFetcherJobHolder) + } + + private fun addWalletClick() { + updateSelectState { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + content = AddWalletBottomSheetContentUM( + onOptionClick = ::onAddWalletOptionClick, + ), + onDismissRequest = { + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + }, + ), + ) + } + } + + private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) { + when (option) { + Create -> router.push(AppRoute.CreateWalletSelection) + Add -> router.push(AppRoute.AddExistingWallet) + Buy -> { + } + } + } + + private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { + val userWalletsWithLock = userWalletsListRepository.userWalletsSync().filter { it.isLocked } + if (userWalletsWithLock.size != 1) return false + val wallet = userWalletsWithLock.first() + return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + } + + private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch { + if (userWallet.isLocked.not()) { + // If the wallet is not locked, we can proceed to the wallet screen directly + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) + return@launch + } + + val unlockMethod = when (userWallet) { + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan + is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode + } + + unlockWallet(userWallet.walletId, unlockMethod) + } + + private fun canUnlockWithBiometrics(): Boolean { + return getIsBiometricsEnabledUseCase.canUseBiometry() + } + + suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { + userWalletsListRepository.unlock(userWalletId, unlockMethod) + .onRight { + userWalletsListRepository.select(userWalletId) + router.replaceAll(AppRoute.Wallet) + } + .onLeft { error -> + error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + } + } + + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: () -> Unit = { }) { + when (this) { + UnlockWalletError.AlreadyUnlocked -> { + // this should not happen, as we check for locked state before this + specificWalletId?.let { userWalletsListRepository.select(it) } + router.replaceAll(AppRoute.Wallet) + } + UnlockWalletError.ScannedCardWalletNotMatched -> { + // TODO Scanned card does not match the wallet + } + UnlockWalletError.UnableToUnlock -> { + // TODO Unable to unlock the wallet" + } + UnlockWalletError.UserCancelled -> onUserCancelled() + UnlockWalletError.UserWalletNotFound -> { + // This should never happen in this flow, as we always check for the wallet existence before unlocking + Timber.e("User wallet not found for unlock: $specificWalletId") + uiMessageSender.send( + SnackbarMessage(TextReference.Res(R.string.generic_error)), + ) + } + } + } + + private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) { + uiState.update { currentState -> + if (currentState is WelcomeUM.SelectWallet) { + block(currentState) + } else { + currentState + } + } + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index caf6ecd9bd..3427544768 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -10,10 +10,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.TextReference +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.ui.state.WalletUM +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.persistentListOf @@ -34,10 +35,6 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) - is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode( - state = st, - modifier = modifier, - ) } } } @@ -49,22 +46,30 @@ private fun Preview() { TangemThemePreview { val state = WelcomeUM.SelectWallet( wallets = persistentListOf( - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("3 cards"), - imageState = WalletUM.ImageState.Loading, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Loading, + balance = UserWalletItemUM.Balance.Loaded( + value = "1.2345 BTC", + isFlickering = false, + ), + isEnabled = true, onClick = {}, ), - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("Mobile wallet"), - imageState = WalletUM.ImageState.MobileWallet, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Failed, + imageState = UserWalletItemUM.ImageState.MobileWallet, + balance = UserWalletItemUM.Balance.Locked, + isEnabled = true, onClick = {}, ), ), ) - var currentState by remember { mutableStateOf(WelcomeUM.EnterAccessCode()) } + var currentState by remember { mutableStateOf(WelcomeUM.SelectWallet()) } Box { Welcome(currentState) @@ -74,11 +79,7 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode( - value = "", - onValueChange = {}, - ) - is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt deleted file mode 100644 index 3cd6085b58..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.appbar.TopAppBarButton -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.welcome.impl.ui.state.WelcomeUM - -@Suppress("MagicNumber") -@Composable -internal fun AnimatedContentScope.WelcomeEnterAccessCode( - state: WelcomeUM.EnterAccessCode, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .statusBarsPadding(), - ) { - Column { - TopAppBarButton( - modifier = Modifier - .padding(12.dp), - button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick), - tint = TangemTheme.colors.icon.primary1, - ) - - SpacerH(68.dp) - - Text( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .align(Alignment.CenterHorizontally), - text = "Enter Access Code", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - - SpacerH24() - - Box( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = 6, - isPasswordVisual = true, - value = state.value, - onValueChange = state.onValueChange, - ) - } - } - - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .imePadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Log in with biometric", - onClick = state.onUnlockWithBiometricClick, - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt index 13aaae14db..5b80e192ee 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -9,8 +9,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.welcome.impl.R @Composable @@ -26,4 +28,14 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) { contentDescription = null, ) } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + WelcomePlain( + modifier = Modifier.fillMaxSize(), + ) + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index ed41fe5ad4..b24c0a7b55 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -5,12 +5,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -20,14 +17,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp -import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.WalletUM import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -45,7 +41,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal TitleText() SpacerH12() - var actualWallets by remember { mutableStateOf>(persistentListOf()) } + var actualWallets by remember { mutableStateOf>(persistentListOf()) } Box(modifier = Modifier.weight(1f)) { LazyColumn( @@ -62,9 +58,12 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal verticalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed(actualWallets) { index, walletState -> - WalletItem( + UserWalletItem( + modifier = Modifier.fillMaxWidth(), state = walletState, - modifier = Modifier, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.field.primary, + ), ) } } @@ -165,66 +164,4 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.secondary, ) } -} - -@Suppress("MagicNumber") -@Composable -private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium) - .clickable(onClick = state.onClick) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - WalletImage(state.imageState) - - SpacerW12() - - Column(Modifier.weight(1f)) { - Text( - text = state.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) { - when (state) { - WalletUM.ImageState.MobileWallet -> { - Box( - modifier = modifier - .size(36.dp) - .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> { - CardImage( - imageState = when (state) { - is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork) - WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading - else -> error("") - }, - modifier = modifier, - ) - } - } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt deleted file mode 100644 index 216a28f7d2..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.welcome.impl.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.TextReference -import javax.annotation.concurrent.Immutable - -internal data class WalletUM( - val name: TextReference, - val subtitle: TextReference, - val imageState: ImageState, - val onClick: () -> Unit, -) { - - @Immutable - sealed class ImageState { - data object MobileWallet : ImageState() - data object Loading : ImageState() - data class Image( - val artwork: ArtworkUM, - ) : ImageState() - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 1e2e509d58..390446ba73 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.welcome.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -11,17 +12,10 @@ internal sealed class WelcomeUM { data object Plain : WelcomeUM() data class SelectWallet( - val wallets: ImmutableList = persistentListOf(), + val wallets: ImmutableList = persistentListOf(), val showUnlockWithBiometricButton: Boolean = false, val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, val onUnlockWithBiometricClick: () -> Unit = {}, val addWalletClick: () -> Unit = {}, ) : WelcomeUM() - - data class EnterAccessCode( - val value: String = "", - val onUnlockWithBiometricClick: () -> Unit = {}, - val onValueChange: (String) -> Unit = {}, - val onBackClick: () -> Unit = {}, - ) : WelcomeUM() } \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index 794a8187e6..bc4cd43085 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a +Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 From 3ac26a45b79c9c8785197bcb344292e5a9a737b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 13:37:51 +0300 Subject: [PATCH 112/165] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt | 3 +++ gradle/tangem_dependencies.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt index 9cc8db4191..af00ff8af8 100644 --- a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk { override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = callSdk { changeAuth(unlockHotWallet, auth) } + override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId = + callSdk { removeBiometryAuthIfPresented(id) } + override suspend fun derivePublicKey( unlockHotWallet: UnlockHotWallet, request: DeriveWalletRequest, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1088cbc11..14441c2e44 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-511" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-461" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 294df69e93cf6607e44868c0c7086d4eb63a7bdd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 16:24:15 +0500 Subject: [PATCH 113/165] Updated on 2026-08-14 --- .../CardContextInterceptor.kt | 4 +- .../tap/di/domain/WalletsDomainModule.kt | 6 + .../welcome/redux/WelcomeMiddleware.kt | 2 +- .../domain/card}/analytics/AnalyticsParam.kt | 2 +- .../card}/analytics/IntroductionProcess.kt | 2 +- .../analytics}/ParamCardCurrencyConverter.kt | 16 +- .../com/tangem/domain/card}/analytics/Shop.kt | 4 +- .../wallets/error/SaveFirstColdWalletError.kt | 7 + domain/wallets/build.gradle.kts | 5 + .../GenerateBuyTangemCardLinkUseCase.kt | 23 +++ .../wallets/usecase/SelectWalletUseCase.kt | 2 +- .../impl/build.gradle.kts | 6 + .../CreateWalletSelectionModel.kt | 148 +++++++++++++++- .../ui/CreateWalletSelectionContent.kt | 18 +- features/home/impl/build.gradle.kts | 3 - .../analytics/ParamCardCurrencyConverter.kt | 23 --- .../features/home/impl/model/HomeModel.kt | 67 +++----- features/hot-wallet/impl/build.gradle.kts | 1 + .../start/AddExistingWalletStartModel.kt | 158 +++++++++++++++++- .../start/entity/AddExistingWalletStartUM.kt | 1 + .../start/ui/AddExistingWalletStartContent.kt | 29 +++- 21 files changed, 427 insertions(+), 100 deletions(-) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/AnalyticsParam.kt (86%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/IntroductionProcess.kt (91%) rename {app/src/main/java/com/tangem/tap/common/analytics/converters => domain/card/src/main/kotlin/com/tangem/domain/card/analytics}/ParamCardCurrencyConverter.kt (58%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/Shop.kt (74%) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt delete mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 8f14501047..805090e6c0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -2,12 +2,12 @@ package com.tangem.tap.common.analytics.paramsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.features.home.impl.analytics.IntroductionProcess -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index eff26615ae..5b21957999 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -131,6 +131,12 @@ internal object WalletsDomainModule { ) } + @Provides + @Singleton + fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase { + return GenerateBuyTangemCardLinkUseCase() + } + @Provides @Singleton fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 37690ee484..8a842072d8 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -11,13 +11,13 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt similarity index 86% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt index def2ff2645..40ce107753 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics internal sealed class AnalyticsParam { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt similarity index 91% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 6115389bfd..0cb4a3685c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt index c5c7e0ea3c..2398da3650 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt @@ -1,18 +1,14 @@ -package com.tangem.tap.common.analytics.converters +package com.tangem.domain.card.analytics import com.tangem.blockchain.common.Blockchain +import com.tangem.core.analytics.models.AnalyticsParam.WalletType import com.tangem.domain.card.CardTypesResolver -import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam -/** -[REDACTED_AUTHOR] - */ -class ParamCardCurrencyConverter : Converter { +class ParamCardCurrencyConverter : Converter { - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency + override fun convert(value: CardTypesResolver): WalletType? { + if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency val type = when { value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) @@ -22,6 +18,6 @@ class ParamCardCurrencyConverter : Converter null } ?: return null - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + return WalletType.SingleCurrency(type.value) } } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt similarity index 74% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 632cc66ef2..62c02a56ab 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -1,8 +1,8 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent -internal sealed class Shop( +sealed class Shop( event: String, params: Map = mapOf(), ) : AnalyticsEvent("Shop", event, params) { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt new file mode 100644 index 0000000000..24c29fd411 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SaveFirstColdWalletError { + data object CreateWalletError : SaveFirstColdWalletError + data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError + data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 8fbf7b49dd..a467663bf5 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -37,6 +37,11 @@ dependencies { implementation(tangemDeps.hot.core) // endregion + /** Other libraries */ + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.timber) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt new file mode 100644 index 0000000000..7c80707cb6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.wallets.usecase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class GenerateBuyTangemCardLinkUseCase { + + suspend operator fun invoke(): String = suspendCoroutine { cont -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { id -> + cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id") + } + .addOnFailureListener { + cont.resume(NEW_BUY_WALLET_URL) + } + } + + companion object { + private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index e0654f947c..21a6a76755 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -4,9 +4,9 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.core.wallets.UserWalletsListRepository diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 477a6af3ea..da58579502 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -18,6 +18,12 @@ dependencies { /** Hot Wallet Feature */ implementation(projects.features.hotWallet.api) + /** Project - Domain */ + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.analytics) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 242046b568..23d94a6c78 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -1,19 +1,63 @@ package com.tangem.features.createwalletselection +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onHardwareWalletClick() { - // TODO open card order web page + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onScanClick() { - // TODO open card scanning + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 0c777f9a97..4106c3d8c6 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -18,6 +20,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock( isScanInProgress: Boolean, modifier: Modifier = Modifier, ) { + var buttonWidth by remember { mutableStateOf(0) } + val density = LocalDensity.current + Row( modifier = modifier .fillMaxWidth() @@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock( style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) + TangemButton( modifier = Modifier - .wrapContentWidth(), + .conditional(buttonWidth > 0) { + width(with(density) { buttonWidth.toDp() }) + } + .onGloballyPositioned { coordinates -> + if (buttonWidth == 0) { + buttonWidth = coordinates.size.width + } + }, text = stringResourceSafe(R.string.wallet_create_scan_title), onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 4a4f17d3bf..d06bedde20 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -51,9 +51,6 @@ dependencies { implementation(deps.compose.coil) implementation(deps.decompose.ext.compose) - /** Firebase */ - implementation(deps.firebase.analytics) - /** Tangem libraries */ implementation(tangemDeps.card.android) implementation(tangemDeps.card.core) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt deleted file mode 100644 index 16c104323b..0000000000 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.home.impl.analytics - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.card.CardTypesResolver -import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam - -internal class ParamCardCurrencyConverter : Converter { - - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency - - val type = when { - value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin) - value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!) - else -> null - } ?: return null - - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) - } -} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 61614e3a62..70636eda60 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -1,7 +1,5 @@ package com.tangem.features.home.impl.model -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute @@ -23,20 +21,22 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.home.api.HomeComponent -import com.tangem.features.home.impl.analytics.IntroductionProcess -import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter -import com.tangem.features.home.impl.analytics.Shop import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories @@ -64,16 +64,17 @@ internal class HomeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val router: Router, - private val selectWalletUseCase: SelectWalletUseCase, private val appRouter: AppRouter, private val getUserCountryUseCase: GetUserCountryUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -135,10 +136,9 @@ internal class HomeModel @Inject constructor( private fun onShopClick() { analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) analyticsEventHandler.send(Shop.ScreenOpened) - - Firebase.analytics.appInstanceId - .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } - .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onSearchTokensClick() { @@ -198,24 +198,17 @@ internal class HomeModel @Inject constructor( saveWalletUseCase(userWallet).fold( ifLeft = { - Timber.e(it.toString(), "Unable to save user wallet") + delay(HIDE_PROGRESS_DELAY) setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } }, ifRight = { + setLoading(false) sendSignedInCardAnalyticsEvent(scanResponse) - - // Select the wallet using new mechanism - selectWalletUseCase(userWallet.walletId).fold( - ifLeft = { - Timber.e("Unable to select user wallet: $it") - setLoading(false) - }, - ifRight = { - delay(HIDE_PROGRESS_DELAY) - setLoading(false) - appRouter.replaceAll(AppRoute.Wallet) - }, - ) + appRouter.replaceAll(AppRoute.Wallet) }, ) } @@ -228,7 +221,7 @@ internal class HomeModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = "1", + walletsCount = userWalletsListManager.walletsCount.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) @@ -241,15 +234,9 @@ internal class HomeModel @Inject constructor( fun handleScanError(error: TangemError) { when (error) { - is TangemSdkError.NfcFeatureIsUnavailable -> { - handleNfcFeatureUnavailable() - } - is TangemSdkError -> { - Timber.e(error, "Scan error occurred") - } - else -> { - Timber.e(error, "Error happened") - } + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") } } @@ -261,8 +248,4 @@ internal class HomeModel @Inject constructor( ), ) } - - companion object { - const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index ccc57778cd..2f25fc3478 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.datasource) /** Domain */ + implementation(projects.domain.card) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index 58ea450355..dab15c9946 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -1,18 +1,63 @@ package com.tangem.features.hotwallet.addexistingwallet.start +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletStartModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val saveWalletUseCase: SaveWalletUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletStartComponent.Params = paramsContainer.require() @@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( AddExistingWalletStartUM( + isScanInProgress = false, onBackClick = params.callbacks::onBackClick, onImportPhraseClick = params.callbacks::onImportPhraseClick, - onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ }, - onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + onScanCardClick = ::onScanClick, + onBuyCardClick = ::onShopClick, ), ) + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt index f898f9c1b7..37a5113f35 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.entity internal data class AddExistingWalletStartUM( + val isScanInProgress: Boolean, val onBackClick: () -> Unit, val onImportPhraseClick: () -> Unit, val onScanCardClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index eef14e30c9..6bf693e36b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi title = stringResourceSafe(R.string.wallet_import_scan_title), description = stringResourceSafe(R.string.wallet_import_scan_description), badge = { - Icon( - modifier = Modifier - .padding(top = 2.dp) - .size(20.dp), - painter = painterResource(R.drawable.ic_tangem_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) + if (state.isScanInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) + .padding(2.dp), + color = TangemTheme.colors.text.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp), + painter = painterResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + } }, onClick = state.onScanCardClick, enabled = true, @@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() { TangemThemePreview { AddExistingWalletStartContent( state = AddExistingWalletStartUM( + isScanInProgress = true, onBackClick = {}, onImportPhraseClick = {}, onScanCardClick = {}, From 59f76deacafd9b9092a6090662cddcf4314058b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 18:02:06 +0500 Subject: [PATCH 114/165] Updated on 2026-08-14 --- .../ui/amountScreen/utils/FormatterUtils.kt | 27 ++-- .../marketprice/MarketPriceBlock.kt | 4 +- .../format/bigdecimal/BigDecimalFiatFormat.kt | 5 +- .../core/ui/utils/BigDecimalFormatter.kt | 147 ------------------ .../impl/model/MarketsTokenDetailsModel.kt | 39 +++-- .../converters/ExchangeItemStateConverter.kt | 15 +- .../converters/PricePerformanceConverter.kt | 15 +- .../impl/model/formatter/Formatters.kt | 15 +- .../impl/model/state/QuotesStateUpdater.kt | 10 +- .../block/impl/model/TokenMarketBlockModel.kt | 18 ++- .../converters/MarketsTokenItemConverter.kt | 17 +- .../RewardsValidatorStateConverter.kt | 13 +- .../ShowApprovalBottomSheetTransformer.kt | 13 +- .../presentation/ui/block/StakingFeeBlock.kt | 4 +- .../swap/converters/TokensDataConverter.kt | 13 +- .../tangem/feature/swap/ui/StateBuilder.kt | 9 +- .../ui/components/TokenDetailsBalanceBlock.kt | 6 +- .../SingleWalletMarketPriceConverter.kt | 19 ++- .../VisaTxDetailsBottomSheetConverter.kt | 15 +- .../VisaTxHistoryItemStateConverter.kt | 15 +- 20 files changed, 157 insertions(+), 262 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 9ee08290a8..af9627c4a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -2,9 +2,11 @@ package com.tangem.common.ui.amountScreen.utils import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN +import com.tangem.core.ui.format.bigdecimal.approximateAmount +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { @@ -19,12 +21,19 @@ fun getFiatString( appCurrency: AppCurrency, approximate: Boolean = false, ): String { - if (value == null || rate == null) return EMPTY_BALANCE_SIGN + if (value == null || rate == null) return DASH_SIGN val feeValue = value.multiply(rate) - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - withApproximateSign = approximate, - ) + return feeValue.format { + if (approximate) { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).approximateAmount() + } else { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 52bdac7eab..204f816c64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.utils.StringsSigns.DASH_SIGN /** * Market price block @@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) } } else { - Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + Price(price = DASH_SIGN, modifier = priceModifier) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 1834e10f45..aef02f2fe3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { +/** + * Returns amount with correct scale + */ +fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt deleted file mode 100644 index 744a2d9bf3..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.core.ui.utils - -import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.LOWER_SIGN -import com.tangem.utils.StringsSigns.TILDE_SIGN -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Currency -import java.util.Locale - -@Suppress("LargeClass") -@Deprecated("Use BigDecimal.format") -object BigDecimalFormatter { - - const val EMPTY_BALANCE_SIGN = DASH_SIGN - private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - - private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - - private const val FIAT_MARKET_DEFAULT_DIGITS = 2 - private const val FIAT_MARKET_EXTENDED_DIGITS = 6 - private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 - - private val usdCurrency = Currency.getInstance("USD") - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmount( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, - locale: Locale = Locale.getDefault(), - withApproximateSign: Boolean = false, - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = decimals - minimumFractionDigits = decimals - roundingMode = RoundingMode.HALF_UP - } - - return if (fiatAmount.checkFiatThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(FIAT_FORMAT_THRESHOLD) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), - ) - } - } else { - val formattedAmount = formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - - if (withApproximateSign) { - buildString { - append(TILDE_SIGN) - append(formattedAmount) - } - } else { - formattedAmount - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmountUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val digits = if (fiatAmount.checkFiatThreshold()) { - FIAT_MARKET_EXTENDED_DIGITS - } else { - FIAT_MARKET_DEFAULT_DIGITS - } - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = digits - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatPriceUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = finalScale - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(formattedAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { - return if (value < BigDecimal.ONE) { - val leadingZeroes = value.scale() - value.precision() - val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES - - val amount = value - .setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - - amount to amount.scale() - } else { - value to FIAT_MARKET_DEFAULT_DIGITS - } - } - - private fun getCurrency(code: String): Currency { - return runCatching { Currency.getInstance(code) } - .getOrElse { e -> - // Currency code is not valid ISO 4217 code - if (e is IllegalArgumentException) { - usdCurrency - } else { - throw e - } - } - } - - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 5e4e7a4d4d..0778b806c6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -18,9 +18,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -164,11 +165,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( type = percentChangeType.toChartType(), xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> - BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = value, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ) + value.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + } }, ) } @@ -196,11 +198,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = params.token.tokenQuotes.currentPrice, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + priceText = params.token.tokenQuotes.currentPrice.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, dateTimeText = resourceReference(R.string.common_today), priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), @@ -403,7 +406,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( state.update { it.copy( - priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceText = newInfo.quotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + }, priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( interval = it.selectedInterval, ), @@ -490,7 +498,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: getDefaultDateTimeString(currentState.selectedInterval) - val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value) + val priceText = (price ?: currentQuotes.value.currentPrice).format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + } val percent = price?.let { getChangePercentBetween( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt index 0ddafb4e83..eb55abd9dd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt @@ -5,7 +5,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter +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.domain.markets.TokenMarketExchange import com.tangem.domain.markets.TokenMarketExchange.TrustScore import com.tangem.features.markets.impl.R @@ -29,11 +31,12 @@ internal object ExchangeItemStateConverter : Converter h24ChangePercent @@ -66,8 +57,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi } internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first - val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first + val current = getFiatPriceAmountWithScale(value = currentPrice).first + val updated = getFiatPriceAmountWithScale(value = updatedPrice).first return when { updated > current -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt index 4bf8b58e27..74d10c5742 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -3,6 +3,9 @@ package com.tangem.features.markets.details.impl.model.state import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +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.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -57,7 +60,12 @@ internal class QuotesStateUpdater( state.update { stateToUpdate -> stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceText = newQuotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency().symbol, + fiatCurrencyCode = currentAppCurrency().code, + ).price() + }, priceChangePercentText = newQuotes.getFormattedPercentByInterval( interval = stateToUpdate.selectedInterval, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 372309d5ea..bb1496cfe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,9 +11,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetCurrencyQuotesUseCase @@ -86,13 +87,14 @@ internal class TokenMarketBlockModel @Inject constructor( ) state.value = state.value.copy( - currentPrice = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = res.fiatRate, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencyCode = currentAppCurrency.value.code, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + currentPrice = res.fiatRate.format { + fiat( + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencyCode = currentAppCurrency.value.code, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index d7352826b5..22712a972c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -7,11 +7,7 @@ import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.impl.R @@ -94,11 +90,12 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = tokenQuotesShort.currentPrice, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } val changeType = if (prevPrice != null) { if (tokenQuotesShort.currentPrice > prevPrice) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 3643ba7e27..4ab5ef020a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -2,8 +2,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -69,11 +69,12 @@ internal class RewardsValidatorStateConverter( }, ) val formattedFiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ) return BalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index c70f2da7a7..c39ac9a72f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -6,8 +6,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.R @@ -38,11 +38,12 @@ internal class ShowApprovalBottomSheetTransformer( val feeCryptoValue = fee.amount.value.format { crypto(fee.amount.currencySymbol, fee.amount.decimals) } - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), - fiatCurrencyCode = appCurrencyProvider().code, - fiatCurrencySymbol = appCurrencyProvider().symbol, - ) + val feeFiatValue = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value).format { + fiat( + fiatCurrencyCode = appCurrencyProvider().code, + fiatCurrencySymbol = appCurrencyProvider().symbol, + ) + } return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( isShown = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index bce18ef1ec..9c5085c6b6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -26,9 +26,9 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal @Composable @@ -126,7 +126,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { ) { if (it == FeeState.Error) { Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d31fdcffe5..86245f0585 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -3,8 +3,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -111,10 +111,11 @@ class TokensDataConverter( } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = cryptoCurrencyStatus.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9f250ee99b..0de047350a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency @@ -1273,7 +1273,12 @@ internal class StateBuilder( private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) + return amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ae100c11a3..d1110e97fa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -20,11 +20,11 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.toImmutableList @Suppress("DestructuringDeclarationWithTooManyEntries") @@ -124,7 +124,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -154,7 +154,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 8dadf5a69e..a2dc7210ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,11 +4,13 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( @@ -47,17 +49,18 @@ internal class SingleWalletMarketPriceConverter( } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatRate = status.fiatRate ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = fiatRate, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatRate.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).uncapped() + } } private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { - val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val priceChange = status.priceChange ?: return DASH_SIGN return priceChange.format { percent() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 99b787897e..f1ea722bd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -2,13 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxDetails -import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import org.joda.time.DateTimeZone @@ -71,11 +71,12 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrency.currencyCode, - fiatCurrencySymbol = fiatCurrency.symbol, - ) + return amount.format { + fiat( + fiatCurrencyCode = fiatCurrency.currencyCode, + fiatCurrencySymbol = fiatCurrency.symbol, + ) + } } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 62d00e4c3c..0ec063a2f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -4,13 +4,13 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.impl.R import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -29,11 +29,12 @@ internal class VisaTxHistoryItemStateConverter( txHash = value.id, amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time - time = BigDecimalFormatter.formatFiatAmount( - fiatAmount = value.fiatAmount, - fiatCurrencyCode = value.fiatCurrency.currencyCode, - fiatCurrencySymbol = value.fiatCurrency.symbol, - ), + time = value.fiatAmount.format { + fiat( + fiatCurrencyCode = value.fiatCurrency.currencyCode, + fiatCurrencySymbol = value.fiatCurrency.symbol, + ) + }, status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, From 7fe2a4d82326cd19a32d7dbdf83edaee45d34ca9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 18:13:29 +0300 Subject: [PATCH 115/165] Updated on 2026-08-14 --- .../core/ui/components/fields/PinTextField.kt | 75 ++++++++++++++----- .../hotwallet/accesscode/ui/AccessCode.kt | 2 + .../DefaultHotAccessCodeRequestComponent.kt | 3 +- .../HotAccessCodeRequestModel.kt | 18 ++++- .../entity/HotAccessCodeRequestUM.kt | 4 +- .../HotAccessCodeRequestFullScreenContent.kt | 15 +++- 6 files changed, 89 insertions(+), 28 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index fa00fb9959..0a593a7a8d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields import androidx.compose.animation.* import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -36,9 +37,9 @@ fun PinTextField( value: String, length: Int, isPasswordVisual: Boolean, + pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - wrongCode: Boolean = false, ) { val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { @@ -72,7 +73,7 @@ fun PinTextField( CellDecoration( length = length, isPasswordVisual = isPasswordVisual, - wrongCode = wrongCode, + pinTextColor = pinTextColor, value = value, ) }, @@ -84,17 +85,25 @@ fun PinTextField( } } -@Suppress("MagicNumber") +enum class PinTextColor { + Primary, + WrongCode, + Success, +} + +@Suppress("MagicNumber", "LongMethod") @Composable private fun CellDecoration( length: Int, - wrongCode: Boolean, + pinTextColor: PinTextColor, value: String, modifier: Modifier = Modifier, isPasswordVisual: Boolean = false, ) { val textMeasurer = rememberTextMeasurer() - val width = textMeasurer.measure("0") + val minSize = textMeasurer.measure("0") + val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell + val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell Row( modifier = modifier, @@ -107,6 +116,18 @@ private fun CellDecoration( "" } + val color = when (pinTextColor) { + PinTextColor.Primary -> { + if (isPasswordVisual) { + TangemTheme.colors.icon.informative + } else { + TangemTheme.colors.text.primary1 + } + } + PinTextColor.WrongCode -> TangemTheme.colors.icon.warning + PinTextColor.Success -> TangemTheme.colors.icon.accent + } + Box( modifier = Modifier .background( @@ -119,26 +140,34 @@ private fun CellDecoration( targetState = char, transitionSpec = { ( - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - slideInVertically(animationSpec = tween(330, delayMillis = 0)) + fadeIn(animationSpec = tween(90, delayMillis = 90)) + + slideInVertically(animationSpec = tween(220, delayMillis = 0)) ) .togetherWith( fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), ) }, ) { text -> - Text( - modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), - text = text, - style = TangemTheme.typography.h3, - color = if (wrongCode) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.primary1 - }, - textAlign = TextAlign.Center, - lineHeight = 48.sp, - ) + if (isPasswordVisual && text.isNotEmpty()) { + Canvas( + Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + ) { + drawCircle( + color = color, + radius = 4.dp.toPx(), + center = center, + ) + } + } else { + Text( + modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + text = text, + style = TangemTheme.typography.h3, + color = color, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } } } } @@ -152,10 +181,18 @@ private fun Preview() { var text by remember { mutableStateOf("123") } Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = true, + pinTextColor = PinTextColor.Success, + length = 6, + ) PinTextField( value = text, onValueChange = { text = it }, isPasswordVisual = false, + pinTextColor = PinTextColor.Primary, length = 6, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 987c94c953..230f97bab4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.res.R import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -75,6 +76,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { length = state.accessCodeLength, isPasswordVisual = true, value = state.accessCode, + pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index 5bacfc9fef..6777d34243 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -26,8 +26,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( } override suspend fun successfulAuthentication() { - // TODO handle successful authentication - // TODO add delay + model.successfulAuthentication() } override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 305bd305bb..f210270b41 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM @@ -45,13 +46,23 @@ internal class HotAccessCodeRequestModel @Inject constructor( suspend fun wrongAccessCode() { uiState.update { it.copy( - wrongAccessCode = true, + accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, ) } delay(timeMillis = 500) // Delay to show the wrong access code state } + suspend fun successfulAuthentication() { + uiState.update { + it.copy( + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + ) + } + delay(timeMillis = 200) // Delay to show the success state + } + private fun getInitialState() = HotAccessCodeRequestUM( onDismiss = ::dismiss, onAccessCodeChange = ::onAccessCodeChange, @@ -66,7 +77,10 @@ internal class HotAccessCodeRequestModel @Inject constructor( if (accessCode.length > ACCESS_CODE_LENGTH) return uiState.update { - it.copy(accessCode = accessCode, wrongAccessCode = false) + it.copy( + accessCode = accessCode, + accessCodeColor = PinTextColor.Primary, + ) } if (accessCode.length == ACCESS_CODE_LENGTH) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 82b7e51ec8..62f2b4d051 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.hotwallet.accesscoderequest.entity +import com.tangem.core.ui.components.fields.PinTextColor + internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", - val wrongAccessCode: Boolean = false, + val accessCodeColor: PinTextColor = PinTextColor.Primary, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bc64b660c4..9ae3230ec4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect @@ -85,7 +86,7 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM length = 6, isPasswordVisual = true, value = state.accessCode, - wrongCode = state.wrongAccessCode, + pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) } @@ -106,9 +107,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val hapticManager = LocalHapticManager.current - LaunchedEffect(state.wrongAccessCode) { - if (state.wrongAccessCode) { - hapticManager.perform(TangemHapticEffect.View.Reject) + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + hapticManager.perform(TangemHapticEffect.View.Reject) + } + PinTextColor.Success -> { + hapticManager.perform(TangemHapticEffect.View.Confirm) + } + else -> Unit } } } From bb954202243cc1e5487672c7cbbdef44863d29e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 09:55:03 +0300 Subject: [PATCH 116/165] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 3ac868e93f..7d225a195e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 +Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c From e409fc4e0edd875bcceff2a0fe0b2ac29a4df93d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 12:57:51 +0500 Subject: [PATCH 117/165] Updated on 2026-08-14 --- .../buttons/small/TangemIconButton.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt index 39a042f707..c0d2dac09e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt @@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,20 +42,19 @@ fun TangemIconButton( background: Color = TangemTheme.colors.button.secondary, iconTint: Color = TangemTheme.colors.icon.secondary, ) { - IconButton( - onClick = onClick, + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), + contentDescription = "", + tint = iconTint, modifier = modifier + .size(24.dp) .clip(shape) .background(background) - .size(24.dp), - ) { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), - contentDescription = "", - tint = iconTint, - modifier = Modifier.size(16.dp), - ) - } + .padding(4.dp) + .clickable( + onClick = onClick, + ), + ) } // region Preview From 39a190f0560f2a93e6e49c998b29630642d7e104 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 10:12:01 +0200 Subject: [PATCH 118/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/TokensDomainModule.kt | 17 ++++++ .../tap/di/domain/TransactionDomainModule.kt | 22 ++++++++ .../di/TokenReceiveWarningModule.kt | 44 +++++++++++++++ .../DefaultTokenReceiveWarningActionStore.kt | 17 ++++++ .../token/TokenReceiveWarningActionStore.kt | 8 +++ .../tangem/data/tokens/di/TokensDataModule.kt | 13 +++++ ...ultTokenReceiveWarningsViewedRepository.kt | 17 ++++++ .../DefaultWalletAddressServiceRepository.kt | 35 ++++++++++++ .../tangem/domain/models/ens/EnsAddress.kt | 7 +++ .../GetViewedTokenReceiveWarningUseCase.kt | 11 ++++ .../SaveViewedTokenReceiveWarningUseCase.kt | 11 ++++ .../TokenReceiveWarningsViewedRepository.kt | 8 +++ .../WalletAddressServiceRepository.kt | 11 +++- .../transaction/usecase/GetEnsNameUseCase.kt | 31 +++++++++++ .../GetReverseResolvedEnsAddressUseCase.kt | 41 ++++++++++++++ features/token-recieve/api/.gitignore | 1 + features/token-recieve/api/build.gradle.kts | 23 ++++++++ features/token-recieve/impl/.gitignore | 1 + features/token-recieve/impl/build.gradle.kts | 55 +++++++++++++++++++ settings.gradle.kts | 3 + tangem-android-tools | 2 +- 22 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt create mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt create mode 100644 features/token-recieve/api/.gitignore create mode 100644 features/token-recieve/api/build.gradle.kts create mode 100644 features/token-recieve/impl/.gitignore create mode 100644 features/token-recieve/impl/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31cb895042..a94d7fc4b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -238,6 +238,8 @@ dependencies { implementation(projects.features.home.impl) implementation(projects.features.account.api) implementation(projects.features.account.impl) + implementation(projects.features.tokenRecieve.api) + implementation(projects.features.tokenRecieve.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 089dc2bf5d..cf5e9e0af4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -24,6 +24,7 @@ import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles @@ -506,4 +507,20 @@ internal object TokensDomainModule { fun provideGetAssetRequirementsUseCase(walletManagersFacade: WalletManagersFacade): GetAssetRequirementsUseCase { return GetAssetRequirementsUseCase(walletManagersFacade) } + + @Provides + @Singleton + fun provideGetViewedTokenReceiveWarningUseCase( + tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, + ): GetViewedTokenReceiveWarningUseCase { + return GetViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository) + } + + @Provides + @Singleton + fun provideSaveViewedTokenReceiveWarningUseCase( + tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, + ): SaveViewedTokenReceiveWarningUseCase { + return SaveViewedTokenReceiveWarningUseCase(tokenReceiveWarningsViewedRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index ffd34ca773..8b0266061b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module @@ -18,6 +19,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object TransactionDomainModule { @@ -210,4 +212,24 @@ internal object TransactionDomainModule { ): CreateNFTTransferTransactionUseCase { return CreateNFTTransferTransactionUseCase(transactionRepository) } + + @Provides + @Singleton + fun provideGetEnsNameUseCase( + walletManagersFacade: WalletManagersFacade, + walletAddressServiceRepository: WalletAddressServiceRepository, + ): GetEnsNameUseCase { + return GetEnsNameUseCase( + walletManagersFacade = walletManagersFacade, + walletAddressServiceRepository = walletAddressServiceRepository, + ) + } + + @Provides + @Singleton + fun provideGetReverseResolvedEnsAddressUseCase( + walletAddressServiceRepository: WalletAddressServiceRepository, + ): GetReverseResolvedEnsAddressUseCase { + return GetReverseResolvedEnsAddressUseCase(walletAddressServiceRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt new file mode 100644 index 0000000000..c0aa4159dd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TokenReceiveWarningModule.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.di + +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.token.DefaultTokenReceiveWarningActionStore +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.setTypes +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object TokenReceiveWarningModule { + + @Provides + @Singleton + fun provideTokenReceiveWarningStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): TokenReceiveWarningActionStore { + return DefaultTokenReceiveWarningActionStore( + persistenceStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = setTypes(), + defaultValue = emptySet(), + ), + produceFile = { context.dataStoreFile(fileName = "token_receive_warnings_viewed") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt new file mode 100644 index 0000000000..8be4bb214a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultTokenReceiveWarningActionStore.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.local.token + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.firstOrNull + +internal class DefaultTokenReceiveWarningActionStore( + private val persistenceStore: DataStore>, +) : TokenReceiveWarningActionStore { + + override suspend fun getSync(): Set { + return persistenceStore.data.firstOrNull() ?: emptySet() + } + + override suspend fun store(symbol: String) { + persistenceStore.updateData { data -> data.plus(symbol) } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt new file mode 100644 index 0000000000..d7ef18221d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/TokenReceiveWarningActionStore.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.local.token + +interface TokenReceiveWarningActionStore { + + suspend fun getSync(): Set + + suspend fun store(symbol: String) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 2cba8e6ef9..fe5dd5cc34 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -8,14 +8,17 @@ import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository import com.tangem.data.tokens.repository.DefaultPolkadotAccountHealthCheckRepository +import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -83,4 +86,14 @@ internal object TokensDataModule { dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideTokenReceiveWarningsViewedRepository( + tokenReceiveWarningActionStore: TokenReceiveWarningActionStore, + ): TokenReceiveWarningsViewedRepository { + return DefaultTokenReceiveWarningsViewedRepository( + tokenReceiveWarningActionStore = tokenReceiveWarningActionStore, + ) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt new file mode 100644 index 0000000000..0d31e13a55 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokenReceiveWarningsViewedRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.data.tokens.repository + +import com.tangem.datasource.local.token.TokenReceiveWarningActionStore +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +internal class DefaultTokenReceiveWarningsViewedRepository( + private val tokenReceiveWarningActionStore: TokenReceiveWarningActionStore, +) : TokenReceiveWarningsViewedRepository { + + override suspend fun getViewedWarnings(): Set { + return tokenReceiveWarningActionStore.getSync() + } + + override suspend fun view(symbol: String) { + tokenReceiveWarningActionStore.store(symbol) + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 48cef0138b..e22b05cb02 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.NameResolver import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.blockchain.common.ReverseResolveAddressResult import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -22,6 +23,40 @@ class DefaultWalletAddressServiceRepository( private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { + override suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String? { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + walletManager?.wallet?.ens + } + } + + override suspend fun reverseResolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ReverseResolveAddressResult { + return withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + if (walletManager is NameResolver) { + walletManager.reverseResolve(address.toByteArray()) + } else { + ReverseResolveAddressResult.NotSupported + } + } + } + override suspend fun resolveAddress( userWalletId: UserWalletId, network: Network, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt new file mode 100644 index 0000000000..d1ab13dded --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/ens/EnsAddress.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.models.ens + +sealed interface EnsAddress { + data class Address(val name: String) : EnsAddress + data class Error(val error: Exception) : EnsAddress + data object NotSupported : EnsAddress +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt new file mode 100644 index 0000000000..7665d02abc --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetViewedTokenReceiveWarningUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +class GetViewedTokenReceiveWarningUseCase( + private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, +) { + suspend operator fun invoke(): Set { + return tokenReceiveWarningsViewedRepository.getViewedWarnings() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt new file mode 100644 index 0000000000..8827166743 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/SaveViewedTokenReceiveWarningUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository + +class SaveViewedTokenReceiveWarningUseCase( + private val tokenReceiveWarningsViewedRepository: TokenReceiveWarningsViewedRepository, +) { + suspend operator fun invoke(symbol: String) { + tokenReceiveWarningsViewedRepository.view(symbol) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt new file mode 100644 index 0000000000..02765acfe4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenReceiveWarningsViewedRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.repository + +interface TokenReceiveWarningsViewedRepository { + + suspend fun getViewedWarnings(): Set + + suspend fun view(symbol: String) +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index 2dfc380480..f566a5a93b 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -1,15 +1,24 @@ package com.tangem.domain.transaction import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.blockchain.common.ReverseResolveAddressResult import com.tangem.domain.models.network.Network -import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.ParsedQrCode /** * Wallet address service repository. */ interface WalletAddressServiceRepository { + suspend fun getEns(userWalletId: UserWalletId, network: Network, address: String): String? + + suspend fun reverseResolveAddress( + userWalletId: UserWalletId, + network: Network, + address: String, + ): ReverseResolveAddressResult + suspend fun resolveAddress(userWalletId: UserWalletId, network: Network, address: String): ResolveAddressResult suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt new file mode 100644 index 0000000000..27a9fd1bfc --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEnsNameUseCase.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.walletmanager.WalletManagersFacade + +class GetEnsNameUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val walletAddressServiceRepository: WalletAddressServiceRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network, address: String): String? { + if (network.nameResolvingType != Network.NameResolvingType.ENS) { + return null + } + + val addresses = walletManagersFacade.getAddresses(userWalletId, network) + + val isOwnAddress = addresses.any { it.value == address } + if (!isOwnAddress) { + return null + } + + return walletAddressServiceRepository.getEns( + userWalletId = userWalletId, + network = network, + address = address, + ) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt new file mode 100644 index 0000000000..93f053818e --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetReverseResolvedEnsAddressUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.transaction.usecase + +import com.tangem.blockchain.common.ReverseResolveAddressResult +import com.tangem.domain.models.ens.EnsAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.supervisorScope + +class GetReverseResolvedEnsAddressUseCase(private val walletAddressServiceRepository: WalletAddressServiceRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + addresses: List, + ): List { + return supervisorScope { + val addressesDeferred = addresses.map { address -> + async { + val result = walletAddressServiceRepository.reverseResolveAddress( + userWalletId = userWalletId, + network = network, + address = address, + ) + mapReverseResolveResult(result) + } + } + addressesDeferred.awaitAll() + } + } + + private fun mapReverseResolveResult(reverseResolveAddressResult: ReverseResolveAddressResult): EnsAddress { + return when (reverseResolveAddressResult) { + is ReverseResolveAddressResult.Error -> EnsAddress.Error(reverseResolveAddressResult.error) + ReverseResolveAddressResult.NotSupported -> EnsAddress.NotSupported + is ReverseResolveAddressResult.Resolved -> EnsAddress.Address(reverseResolveAddressResult.name) + } + } +} \ No newline at end of file diff --git a/features/token-recieve/api/.gitignore b/features/token-recieve/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/token-recieve/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/token-recieve/api/build.gradle.kts b/features/token-recieve/api/build.gradle.kts new file mode 100644 index 0000000000..5b31ee2af9 --- /dev/null +++ b/features/token-recieve/api/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.tokenrecieve.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Common */ + implementation(projects.common.ui) + + /** Domain */ + implementation(projects.domain.models) + +} \ No newline at end of file diff --git a/features/token-recieve/impl/.gitignore b/features/token-recieve/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/token-recieve/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts new file mode 100644 index 0000000000..d46cd33aad --- /dev/null +++ b/features/token-recieve/impl/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tokenreceive.impl" +} + +dependencies { + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + implementation(deps.lifecycle.compose) + implementation(deps.kotlin.serialization) + implementation(deps.decompose.ext.compose) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Common */ + implementation(projects.common.ui) + + /** Core modules */ + implementation(projects.common.routing) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.decompose) + implementation(projects.core.res) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) + + /** Feature Apis */ + implementation(projects.features.tokenRecieve.api) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2e4a4b6f20..9e2dd16bde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -272,6 +272,9 @@ include(":features:welcome:impl") include(":features:account:api") include(":features:account:impl") + +include(":features:token-recieve:api") +include(":features:token-recieve:impl") // endregion Feature modules // region Domain modules diff --git a/tangem-android-tools b/tangem-android-tools index 794a8187e6..428b83bb37 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a +Subproject commit 428b83bb378b615209e23afa05c88c454d06a9f1 From 694c215bb1a5ab439af9faa8d8b14394b974ecde Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:36:50 +0500 Subject: [PATCH 119/165] Updated on 2026-08-14 --- .../components/inputrow/InputRowRecipient.kt | 2 +- .../ui/components/rows/SelectorRowItem.kt | 9 +- .../send/v2/api/entity/FeeSelectorUM.kt | 35 ++++- .../features/send/v2/common/ui/FeeBlock.kt | 70 +++++++++ .../features/send/v2/common/ui/SendContent.kt | 8 +- .../ui/FeeSelectorModalBottomSheet.kt | 140 +++++------------- .../send/v2/send/DefaultSendComponent.kt | 21 --- .../success/SendConfirmSuccessComponent.kt | 4 - .../success/ui/SendConfirmSuccessContent.kt | 10 +- .../send/v2/subcomponents/fee/ui/FeeBlock.kt | 32 ++-- .../fee/ui/SendSpeedSelectorItem.kt | 3 +- .../presentation/ui/block/StakingFeeBlock.kt | 7 +- .../success/ui/SendWithSwapSuccessContent.kt | 9 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 6 +- 14 files changed, 178 insertions(+), 178 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index 418341d870..259553da0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -76,7 +76,7 @@ fun InputRowRecipient( val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning } else { - title to TangemTheme.colors.text.secondary + title to TangemTheme.colors.text.tertiary } DividerContainer( modifier = modifier, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 1499aaecaf..be5201fa5c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,7 +22,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags @@ -31,7 +30,7 @@ import com.tangem.utils.StringsSigns @Composable fun SelectorRowItem( - @StringRes titleRes: Int, + title: TextReference, @DrawableRes iconRes: Int, modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12), @@ -80,7 +79,7 @@ fun SelectorRowItem( contentDescription = null, ) Text( - text = stringResourceSafe(titleRes), + text = title.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), @@ -151,7 +150,7 @@ private fun RowScope.SelectorValueContent( private fun SelectorRowItemPreview() { TangemThemePreview { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, + title = resourceReference(R.string.common_fee_selector_option_slow), iconRes = R.drawable.ic_tortoise_24, preDot = TextReference.Str("1000 ETH"), postDot = TextReference.Str("1000 $"), diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index ca50585ecf..89ddb338b6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -5,8 +5,10 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.R import com.tangem.features.send.v2.api.entity.FeeItem.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -72,14 +74,37 @@ sealed class FeeNonce { @Immutable sealed class FeeItem { abstract val fee: Fee + abstract val title: TextReference + abstract val iconRes: Int fun isSameClass(other: FeeItem): Boolean { return this::class == other::class } - data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() - data class Slow(override val fee: Fee) : FeeItem() - data class Market(override val fee: Fee) : FeeItem() - data class Fast(override val fee: Fee) : FeeItem() - data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() + data class Suggested( + override val title: TextReference, + override val fee: Fee, + ) : FeeItem() { + override val iconRes: Int = R.drawable.ic_star_mini_24 + } + + data class Slow(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_slow) + override val iconRes: Int = R.drawable.ic_tortoise_24 + } + + data class Market(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market) + override val iconRes: Int = R.drawable.ic_bird_24 + } + + data class Fast(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_fast) + override val iconRes: Int = R.drawable.ic_hare_24 + } + + data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_custom) + override val iconRes: Int = R.drawable.ic_edit_v2_24 + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt new file mode 100644 index 0000000000..473a7b235e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R + +@Composable +internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + val feeExtraInfo = feeSelectorUM.feeExtraInfo + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount + SelectorRowItem( + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, + preDot = remember { + stringReference( + feeAmount.value.format { + crypto( + symbol = feeAmount.currencySymbol, + decimals = feeAmount.decimals, + ).fee(canBeLower = feeExtraInfo.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { + getFiatReference(feeAmount.value, feeFiatRateUM.rate, feeFiatRateUM.appCurrency) + } else { + null + } + }, + ellipsizeOffset = feeAmount.currencySymbol.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 0f860e75fc..610020a85c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -18,8 +18,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.send.confirm.SendConfirmComponent -import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent @Composable internal fun SendContent( @@ -38,9 +36,9 @@ internal fun SendContent( Children( stack = stackState, animation = stackAnimation { child -> - when (child.instance) { - is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f) - is SendConfirmComponent -> fade() + when (child.configuration) { + is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f) + is CommonSendRoute.Confirm -> fade() else -> slide() } }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 8f3ef2cb54..36ae4fc32f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -115,7 +115,6 @@ private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDis } } -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( state: FeeSelectorUM.Content, @@ -144,110 +143,6 @@ private fun FeeSelectorItems( .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { - is FeeItem.Suggested -> RegularFeeItemContent( - modifier = itemModifier, - title = item.title, - iconRes = R.drawable.ic_star_mini_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Slow -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_slow), - iconRes = R.drawable.ic_tortoise_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Market -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_market), - iconRes = R.drawable.ic_bird_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Fast -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_fast), - iconRes = R.drawable.ic_hare_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) is FeeItem.Custom -> CustomFeeBlock( modifier = itemModifier, customFee = item, @@ -257,6 +152,32 @@ private fun FeeSelectorItems( onValueChange = feeSelectorIntents::onCustomFeeValueChange, nonce = state.feeNonce, ) + else -> RegularFeeItemContent( + modifier = itemModifier, + title = item.title, + iconRes = item.iconRes, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + preDot = stringReference( + item.fee.amount.value.format { + crypto( + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + }, + ), + postDot = if (feeFiatRateUM != null) { + getFiatReference( + value = item.fee.amount.value, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, + ) + } else { + null + }, + ellipsizeOffset = item.fee.amount.currencySymbol.length, + showDivider = !isSelected && !lastItem, + ) } } } @@ -491,7 +412,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), - FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))), + FeeItem.Market( + fee = Fee.Common( + Amount( + value = BigDecimal("0.02"), + blockchain = Blockchain.Ethereum, + ), + ), + ), FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 9ac55d3a43..74c7fa09ce 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -38,7 +38,6 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted @@ -254,7 +253,6 @@ internal class DefaultSendComponent @AssistedInject constructor( val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value if (sendAmount == null || destinationAddress == null || @@ -279,29 +277,10 @@ internal class DefaultSendComponent @AssistedInject constructor( onClick = {}, ) - val feeBlockComponent = SendFeeBlockComponent( - appComponentContext = child("sendConfirmFeeBlock"), - params = SendFeeComponentParams.FeeBlockParams( - state = model.uiState.value.feeUM, - analyticsCategoryName = model.analyticCategoryName, - userWallet = model.userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - appCurrency = model.appCurrency, - sendAmount = sendAmount, - destinationAddress = destinationAddress, - blockClickEnableFlow = MutableStateFlow(true), - onLoadFee = model::loadFee, - ), - onResult = { }, - onClick = {}, - ) - return SendConfirmSuccessComponent( appComponentContext = factoryContext, params = SendConfirmSuccessComponent.Params( sendUMFlow = model.uiState, - feeBlockComponent = feeBlockComponent, destinationBlockComponent = destinationBlockComponent, analyticsCategoryName = model.analyticCategoryName, currentRoute = model.currentRoute, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt index c9fa321928..5468cbf45e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt @@ -12,7 +12,6 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel import com.tangem.features.send.v2.send.success.ui.SendConfirmSuccessContent import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +22,6 @@ internal class SendConfirmSuccessComponent( private val model: SendConfirmSuccessModel = getOrCreateModel(params = params) private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent - private val feeBlockComponent: SendFeeBlockComponent = params.feeBlockComponent @Composable override fun Content(modifier: Modifier) { @@ -31,14 +29,12 @@ internal class SendConfirmSuccessComponent( SendConfirmSuccessContent( sendUM = state, destinationBlockComponent = destinationBlockComponent, - feeBlockComponent = feeBlockComponent, ) } data class Params( val sendUMFlow: StateFlow, val destinationBlockComponent: SendDestinationBlockComponent, - val feeBlockComponent: SendFeeBlockComponent, val analyticsCategoryName: String, val currentRoute: Flow, val txUrl: String, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 179bc3f11b..636828e73f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -21,18 +21,14 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.delay @Composable -internal fun SendConfirmSuccessContent( - sendUM: SendUM, - destinationBlockComponent: SendDestinationBlockComponent, - feeBlockComponent: SendFeeBlockComponent, -) { +internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -84,7 +80,7 @@ internal fun SendConfirmSuccessContent( onClick = {}, ) destinationBlockComponent.Content(modifier = Modifier) - feeBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = sendUM.feeSelectorUM) Spacer(Modifier.height(60.dp)) } BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt index 10284bde92..5aae6a80e5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN @@ -62,20 +64,24 @@ internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 } SelectorRowItem( - titleRes = title, + title = resourceReference(title), iconRes = icon, - preDot = stringReference( - feeAmount?.value.format { - crypto( - symbol = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - ).fee(canBeLower = feeUM.isFeeApproximate) - }, - ), - postDot = if (feeUM.isFeeConvertibleToFiat) { - getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) - } else { - null + preDot = remember { + stringReference( + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeUM.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeUM.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) + } else { + null + } }, ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt index 3cc6d6042b..09ae784965 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN import com.tangem.core.ui.format.bigdecimal.crypto @@ -52,7 +53,7 @@ internal fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { SelectorRowItem( - titleRes = titleRes, + title = resourceReference(titleRes), iconRes = iconRes, onSelect = onSelect, modifier = modifier, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 9c5085c6b6..0ca152acfb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -19,6 +19,7 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto @@ -51,7 +52,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = stringReference( feeAmount?.value.format { @@ -75,7 +76,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Loading -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -85,7 +86,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Error -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index e77b8efd44..6e5406b9e6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -202,16 +202,17 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResourceSafe(com.tangem.common.ui.R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { - val feeAmount = feeSelectorUM.selectedFeeItem.fee.amount + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount SelectorRowItem( - titleRes = com.tangem.common.ui.R.string.common_fee_selector_option_market, - iconRes = com.tangem.common.ui.R.drawable.ic_bird_24, + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, preDot = stringReference( feeAmount.value.format { crypto( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 1932d52634..3946ea9358 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -14,8 +14,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -110,7 +110,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), @@ -122,7 +122,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { } FeeType.PRIORITY -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_fast, + title = resourceReference(R.string.common_fee_selector_option_fast), iconRes = R.drawable.ic_hare_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), From 143dcc5a6287579c15e896ad97b6a0f625a0b932 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:17:04 +0300 Subject: [PATCH 120/165] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 9 ++ .../java/com/tangem/tap/TangemApplication.kt | 12 +++ .../common/redux/legacy/LegacyMiddleware.kt | 15 ++- .../tap/di/domain/WalletsDomainModule.kt | 2 + .../di/UserWalletsListManagerModule.kt | 3 +- .../DefaultUserWalletsListRepository.kt | 85 +++++++++++------ .../UserWalletEncryptionKeysRepository.kt | 26 +++-- .../details/redux/DetailsMiddleware.kt | 95 +++++++++++++++++++ .../features/details/redux/DetailsReducer.kt | 17 ++++ .../features/details/redux/DetailsState.kt | 7 +- .../appsettings/AppSettingsDialogsFactory.kt | 34 +++++++ .../ui/appsettings/AppSettingsItemsFactory.kt | 36 +++++++ .../ui/appsettings/model/AppSettingsModel.kt | 86 +++++++++++++++-- .../tap/proxy/redux/DaggerGraphState.kt | 6 ++ .../local/preferences/PreferencesKeys.kt | 4 + core/res/src/main/res/values-de/strings.xml | 8 +- core/res/src/main/res/values-es/strings.xml | 6 +- core/res/src/main/res/values-fr/strings.xml | 3 +- core/res/src/main/res/values-ja/strings.xml | 13 ++- core/res/src/main/res/values-ru/strings.xml | 8 +- .../src/main/res/values-uk-rUA/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 9 +- .../data/wallets/DefaultWalletsRepository.kt | 65 +++++++++++++ .../data/wallets/hot/HotWalletAccessor.kt | 55 +++++++++-- .../core/wallets/UserWalletsListRepository.kt | 15 ++- .../repositories/SettingsRepository.kt | 2 + .../wallets/repository/WalletsRepository.kt | 10 ++ .../wallets/usecase/SaveWalletUseCase.kt | 20 ++-- features/biometry/impl/build.gradle.kts | 1 + .../biometry/impl/model/AskBiometryModel.kt | 28 +++++- .../hotwallet/accesscode/AccessCodeModel.kt | 28 +++--- .../model/AddExistingWalletImportModel.kt | 2 +- .../welcome/impl/model/WelcomeModel.kt | 10 +- .../welcome/impl/ui/WelcomeSelectWallet.kt | 22 +++-- 34 files changed, 632 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ed9d07c577..aa3d4f2a9a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -142,4 +145,10 @@ interface ApplicationEntryPoint { fun getApiConfigsManager(): ApiConfigsManager fun getUserTokensResponseStore(): UserTokensResponseStore + + fun getUserWalletsListRepository(): UserWalletsListRepository + + fun getTangemHotSdk(): TangemHotSdk + + fun getHotWalletFeatureToggles(): HotWalletFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ef784f8347..09c556cae1 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userTokensResponseStore: UserTokensResponseStore get() = entryPoint.getUserTokensResponseStore() + private val userWalletsListRepository + get() = entryPoint.getUserWalletsListRepository() + + private val tangemHotSdk + get() = entryPoint.getTangemHotSdk() + + private val hotWalletFeatureToggles + get() = entryPoint.getHotWalletFeatureToggles() + // endregion private val appScope = MainScope() @@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat uiMessageSender = uiMessageSender, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, userTokensResponseStore = userTokensResponseStore, + userWalletsListRepository = userWalletsListRepository, + tangemHotSdk = tangemHotSdk, + hotWalletFeatureToggles = hotWalletFeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 15530c62b3..f944a89717 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -26,10 +26,9 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.PrepareDetailsScreen -> { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - userWalletsListManager.selectedUserWallet + selectedUserWallet() .distinctUntilChanged() .onEach { selectedUserWallet -> val initializedAppSettingsStateContent = initializeAppSettingsState( @@ -52,6 +51,16 @@ internal object LegacyMiddleware { } } + private fun selectedUserWallet(): Flow { + val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() + } else { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + userWalletsListManager.selectedUserWallet + } + } + /** * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking * previously it was initialized in runBlocking and blocked details screen @@ -64,6 +73,8 @@ internal object LegacyMiddleware { selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(), + useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(), isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) .getBalanceHidingSettings().isHidingEnabledInSettings, needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 5b21957999..44dd702fc3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -123,10 +123,12 @@ internal object WalletsDomainModule { userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, hotWalletFeatureToggles: HotWalletFeatureToggles, + walletsRepository: WalletsRepository, ): SaveWalletUseCase { return SaveWalletUseCase( userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, + walletsRepository = walletsRepository, useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f6323a20de..d8173d8bc7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -119,6 +119,7 @@ internal object UserWalletsListManagerModule { @ApplicationContext applicationContext: Context, dispatchers: CoroutineDispatcherProvider, passwordRequester: HotWalletPasswordRequester, + appPreferencesStore: AppPreferencesStore, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -162,8 +163,8 @@ internal object UserWalletsListManagerModule { passwordRequester = passwordRequester, userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository, tangemSdkManagerProvider = Provider { tangemSdkManager }, + appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now - // TODO add a settings toggle to disable saving persistent information ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 6d907c384b..d900b3f687 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -8,6 +8,9 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.flatMap import com.tangem.common.map +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -43,6 +46,7 @@ internal class DefaultUserWalletsListRepository( private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository, private val tangemSdkManagerProvider: Provider, private val savePersistentInformation: ProviderSuspend, + private val appPreferencesStore: AppPreferencesStore, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -129,38 +133,46 @@ internal class DefaultUserWalletsListRepository( userWallet } - override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either = - either { - val userWallet = userWallets.value?.find { it.walletId == userWalletId } - ?: raise(SetLockError.UserWalletNotFound) + override suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean, + ): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SetLockError.UserWalletNotFound) - val encryptionKey = userWallet.encryptionKey - ?: raise(SetLockError.UserWalletLocked) + val encryptionKey = userWallet.encryptionKey + ?: raise(SetLockError.UserWalletLocked) - runCatching { - userWalletEncryptionKeysRepository.save( - encryptionKey = UserWalletEncryptionKey( - walletId = userWalletId, - encryptionKey = encryptionKey, - ), - method = when (lockMethod) { - is LockMethod.AccessCode -> { - UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + runCatching { + userWalletEncryptionKeysRepository.save( + encryptionKey = UserWalletEncryptionKey( + walletId = userWalletId, + encryptionKey = encryptionKey, + ), + removeUnsecured = changeUnsecured, + method = when (lockMethod) { + is LockMethod.AccessCode -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + } + LockMethod.Biometric -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric + } + LockMethod.NoLock -> { + if (userWallet is UserWallet.Cold) { + raise(SetLockError.UserWalletNotFound) } - LockMethod.Biometric -> { - UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric - } - LockMethod.NoLock -> { - if (userWallet is UserWallet.Cold) { - raise(SetLockError.UserWalletNotFound) - } - UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured - } - }, - ) - }.onFailure { raise(SetLockError.UnableToSetLock(it)) } - } + UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured + } + }, + ) + }.onFailure { raise(SetLockError.UnableToSetLock(it)) } + } + + override suspend fun removeBiometricLock(userWalletId: UserWalletId) { + userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId) + } override suspend fun delete(userWalletIds: List): Either = either { if (userWalletIds.isEmpty()) return Unit.right() @@ -269,9 +281,11 @@ internal class DefaultUserWalletsListRepository( } val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() - val allKeys = biometricKeys + unsecuredKeys + val allKeys = (biometricKeys + unsecuredKeys).distinct() + val unlockedWallets = allKeys.map { it.walletId } - if (allKeys.all { it.walletId in userWalletIds }.not()) { + // if we cant unlock all wallets + if (userWalletIds.all { it in unlockedWallets }.not()) { raise(UnlockWalletError.UnableToUnlock) } @@ -309,7 +323,7 @@ internal class DefaultUserWalletsListRepository( biometryFallback: suspend () -> Either, ): Either { val result = passwordRequester.requestPassword( - hasBiometry = tangemSdkManagerProvider.invoke().canUseBiometry, + hasBiometry = hasBiometry(), ) return when (result) { @@ -339,6 +353,15 @@ internal class DefaultUserWalletsListRepository( } } + private suspend fun hasBiometry(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + default = false, + ) + + return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication + } + /** * Find the nearest available wallet that can be selected * diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index 43a5b4916f..104cfc0945 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -25,8 +25,14 @@ internal class UserWalletEncryptionKeysRepository( Types.newParameterizedType(List::class.java, UserWalletId::class.java), ) - suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) { - secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + suspend fun save( + encryptionKey: UserWalletEncryptionKey, + removeUnsecured: Boolean = true, + method: EncryptionMethod, + ) = withContext(dispatchers.io) { + if (removeUnsecured) { + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + } when (method) { EncryptionMethod.Unsecured -> { @@ -35,12 +41,6 @@ internal class UserWalletEncryptionKeysRepository( data = encryptionKey.encode(), ) } - EncryptionMethod.Biometric -> { - authenticatedStorage.store( - keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, - data = encryptionKey.encode(), - ) - } is EncryptionMethod.Password -> { val encodedWithPass = AESEncryptionProtocol.encryptWithPassword( password = method.password, @@ -51,11 +51,21 @@ internal class UserWalletEncryptionKeysRepository( data = encodedWithPass, ) } + EncryptionMethod.Biometric -> { + authenticatedStorage.store( + keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } } storeUserWalletId(userWalletId = encryptionKey.walletId) } + fun removeBiometricKey(userWalletId: UserWalletId) { + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + suspend fun getAllUnsecured(): List = withContext(dispatchers.io) { getUserWalletsIds().mapNotNull { userWalletId -> secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3aece96fa9..9281cbc507 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -64,6 +66,14 @@ class DetailsMiddleware { when (action.setting) { AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) + AppSetting.RequireAccessCode -> toggleRequireAccessCode( + state = state, + enable = action.enable, + ) + AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication( + state = state, + enable = action.enable, + ) } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { @@ -90,6 +100,91 @@ class DetailsMiddleware { } } + private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.useBiometricAuthentication() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + toggleRequireAccessCode( + state = state, + enable = true, + ) + + if (enable) { + setBiometricLockForAllWallets() + } else { + // Remove all biometric-related data + removeAllBiometricData() + } + + walletsRepository.setUseBiometricAuthentication(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.requireAccessCode() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + if (enable) { + // Remove all biometric sign data + removeAllBiometricSingData() + toggleSaveAccessCodes(state, enable = false) + } else { + toggleSaveAccessCodes(state, enable = true) + } + + walletsRepository.setRequireAccessCode(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private suspend fun setBiometricLockForAllWallets() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val userWallets = userWalletsListRepository.userWalletsSync() + userWallets.forEach { + userWalletsListRepository.setLock( + userWalletId = it.walletId, + lockMethod = LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + + private suspend fun removeAllBiometricData() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + userWalletsListRepository.userWalletsSync().forEach { + userWalletsListRepository.removeBiometricLock(it.walletId) + } + removeAllBiometricSingData() + } + + private suspend fun removeAllBiometricSingData() { + deleteSavedAccessCodes() + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk) + userWalletsListRepository.userWalletsSync().forEach { + if (it is UserWallet.Hot) { + userWalletsListRepository.saveWithoutLock( + userWallet = it.copy( + hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId), + ), + ) + } + } + } + private fun observeBiometricsStatusChanges(scope: CoroutineScope) { val needEnrollBiometricsFlow = flow { do { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a6f2b28868..e783c18e37 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta ) } +@Suppress("LongMethod", "CyclomaticComplexMethod") private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( @@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail saveWallets = true, // User can't enable access codes saving without wallets saving saveAccessCodes = action.enable, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = true, + requireAccessCode = action.enable, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = true, + useBiometricAuthentication = action.enable, + ) }, ) is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy( @@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail isInProgress = false, saveAccessCodes = action.prevState, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = false, + requireAccessCode = action.prevState, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = false, + needEnrollBiometrics = action.prevState, + ) }, ) is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index bc7f17a7b6..980cacb286 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,9 +12,14 @@ data class DetailsState( ) : StateType data class AppSettingsState( + @Deprecated("Delete after hot wallet release") val saveWallets: Boolean = false, + @Deprecated("Delete after hot wallet release") val saveAccessCodes: Boolean = false, + @Deprecated("Delete after hot wallet release") val isBiometricsAvailable: Boolean = false, + val requireAccessCode: Boolean = false, + val useBiometricAuthentication: Boolean = false, val needEnrollBiometrics: Boolean = false, val isHidingEnabled: Boolean = false, val isInProgress: Boolean = false, @@ -25,5 +30,5 @@ data class AppSettingsState( enum class SecurityOption { LongTap, PassCode, AccessCode } enum class AppSetting { - SaveWallets, SaveAccessCode + SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 43e66b065a..73459bc7b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory { onDismiss = onDismiss, ) } + + fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference( + R.string.app_settings_off_biometrics_alert_message, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + confirmText = resourceReference(R.string.common_disable), + onConfirm = onDisable, + onDismiss = onDismiss, + ) + } + + fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_on_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_enable), + onConfirm = { onEnable() }, + onDismiss = onDismiss, + ) + } + + fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_disable), + onConfirm = { onDisable() }, + onDismiss = onDismiss, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 47bbb8376f..bde3cf116f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings 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.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.wallet.R @@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory { ) } + fun createUseBiometricsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_USE_BIOMETRICS_SWITCH, + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference( + R.string.app_settings_biometrics_footer, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createRequireAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_REQUIRE_ACCESS_CODE_SWITCH, + title = resourceReference(R.string.app_settings_require_access_code), + description = resourceReference(R.string.app_settings_require_access_code_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + fun createSaveAccessCodeSwitch( isChecked: Boolean, isEnabled: Boolean, @@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory { const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch" const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button" const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button" + const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch" + const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index ee93849f31..8b9f0e5a01 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor( private val appThemeModeRepository: AppThemeModeRepository, private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model(), StoreSubscriber { private val itemsFactory = AppSettingsItemsFactory() @@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor( onClick = ::showAppCurrencySelector, ).let(::add) - if (state.isBiometricsAvailable) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress - itemsFactory.createSaveWalletsSwitch( - isChecked = state.saveWallets, + itemsFactory.createUseBiometricsSwitch( + isChecked = state.useBiometricAuthentication, isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveWalletsToggled, + onCheckedChange = ::onBiometricAuthenticationToggled, ).let(::add) - itemsFactory.createSaveAccessCodeSwitch( - isChecked = state.saveAccessCodes, - isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveAccessCodesToggled, + itemsFactory.createRequireAccessCodeSwitch( + isChecked = state.requireAccessCode, + isEnabled = canUseBiometrics && state.useBiometricAuthentication, + onCheckedChange = ::onRequireAccessCodeToggled, ).let(::add) + } else { + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } } itemsFactory.createFlipToHideBalanceSwitch( @@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor( } } + private fun onBiometricAuthenticationToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param)) + if (isChecked) { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = true) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( + onDisable = { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = false) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onRequireAccessCodeToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param)) + updateContentState { + copy( + dialog = if (isChecked) { + dialogsFactory.createEnableRequireAccessCodeAlert( + onEnable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + } else { + dialogsFactory.createDisableRequireAccessCodeAlert( + onDisable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + }, + ) + } + } + private fun onSaveWalletsToggled(isChecked: Boolean) { if (isChecked) { onSettingsToggled(AppSetting.SaveWallets, enable = true) @@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor( saveWallets = walletsRepository.shouldSaveUserWalletsSync(), saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), isBiometricsAvailable = canUseBiometryUseCase(), + useBiometricAuthentication = walletsRepository.useBiometricAuthentication(), + requireAccessCode = walletsRepository.requireAccessCode(), isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 1c696ac842..e0b2794648 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository @@ -77,4 +80,7 @@ data class DaggerGraphState( val cardArworksProvider: CardArtworksProvider? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val userTokensResponseStore: UserTokensResponseStore? = null, + val userWalletsListRepository: UserWalletsListRepository? = null, + val hotWalletFeatureToggles: HotWalletFeatureToggles? = null, + val tangemHotSdk: TangemHotSdk? = null, ) : StateType \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index a737416dd5..6c0e164b4e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -84,6 +84,10 @@ object PreferencesKeys { val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") } + + val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") } + val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 554f254397..38278a60f0 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -947,9 +947,13 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Tauschen und senden + Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht. + Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust. + Wähle das richtige Empfängernetzwerk Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. Wird an den Empfänger gesendet Zu erhaltender Betrag + Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht. Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1408,7 +1412,7 @@ Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem @@ -1432,7 +1436,7 @@ Daten kopieren Benutzerdefinierter Freibetrag Alle trennen - Text über die Trennung aller dApps + Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein. Alle dApps trennen Versuchen Sie erneut, mit einer neuen URI zu koppeln Ungültige dApp-Domain diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b5eeeccbc8..3810328a13 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1354,7 +1354,7 @@ Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Tangem soporta una red requerida por %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema @@ -1382,7 +1382,7 @@ Asignación personalizada dApp desconectada Desconectar todo - Texto sobre desconexión de todas las dApps + Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva Dominio de dApp no válido @@ -1418,7 +1418,7 @@ Cantidad ilimitada Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único URI ya utilizado - Wallet connect + WalletConnect Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8e7d9898e8..900aa6723d 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1324,6 +1324,7 @@ dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes.%s Autoriser à dépenser Adresse Chargement @@ -1333,7 +1334,7 @@ Contenu Copier les données Déconnecter tout - Texte sur la déconnexion de toutes les dApps + Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps Essayez de jumeler à nouveau avec un nouvel URI Domaine dApp invalide diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2986582d9d..a66d2ba966 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,10 +12,13 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + 回復する + アーカイブ済み アカウントをアーカイブする アーカイブ このアカウントをアーカイブしますが、いつでも復元できます。 アカウント + アカウント番号%s — アドレス導出に使用されます。 アカウントを追加 保存 アカウント名 @@ -287,6 +290,7 @@ 取引状況 取引 送金 + データを読み込めません… わかりました エラーが発生しました。もう一度お試しください。 アクセスできません @@ -984,10 +988,13 @@ スワップして送信 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 + その他の通貨を送信すると、取り返しのつかない損失が発生します。 + 正しい受信者ネットワークを選択してください トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受取人へ 受取金額 + 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 変換を削除 スワップして送信 @@ -1448,7 +1455,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - Tangemは%sで必要なネットワークをサポートします + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています @@ -1476,7 +1483,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppsの接続解除に関するテキスト + すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン @@ -1512,7 +1519,7 @@ 無制限 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています - ウォレットコネクト + WalletConnect 不審な取引 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 32374c0f84..f9fdb1b3f2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -231,6 +231,7 @@ Статус транзакции Транзакции Перевод + Невозможно загрузить данные… Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно @@ -1317,12 +1318,12 @@ Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки. Если проблема сохраняется, обратитесь в нашу службу поддержки Мы обнаружили неизвестную ошибку - Кошелек Tangem.в настоящий момент не поддерживает %s + Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Tangem поддерживает сеть, необходимую для %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема @@ -1350,6 +1351,7 @@ Настраиваемый лимит dApp отключен Отключить все + Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp. Отключить все dApp Попробуйте соединиться снова, используя новый URI Недействительный домен dApp @@ -1384,7 +1386,7 @@ Безлимитное количество Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. URI уже используется - Подключение кошелька + WalletConnect Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 0b17da86ca..952d5a7dd6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1304,7 +1304,7 @@ Ми зіткнулися з невідомою помилкою Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Tangem підтримує мережу, необхідну для %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1328,7 +1328,7 @@ Копіювати дані dApp відключено Розʼєднати все - Відключити всі dApps + Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. Відключити всі dApps Спробуйте ще раз з новим URI Недійсний домен dApp diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f0624f96ea..a76469f95b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -71,8 +71,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. + You’ll be asked for your wallet’s access code later so we can securely store it for future use This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. Require Access Code This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code @@ -297,6 +299,7 @@ Transaction status Transactions Transfer + Unable to load the data… I understand There was an error. Please try again. Unreachable @@ -1519,7 +1522,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsupported networks - Tangem support a required network by %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem @@ -1547,7 +1550,7 @@ Custom allowance dApp disconnected Disconnect all - Text about discnected all dApps + All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps. Disconect All dApps Try pairing again with a fresh URI Invalid dApp domain @@ -1584,7 +1587,7 @@ Unlimited Amount Ensure that each pairing attempt uses a fresh and unique URI URI already used - Wallet connect + WalletConnect Suspicious transaction Discard You have an interrupted backup. Do you want to resume? diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..2127d9b80b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.wallet.UserWallet @@ -47,14 +48,78 @@ internal class DefaultWalletsRepository( return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override fun shouldSaveUserWallets(): Flow { return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override suspend fun saveShouldSaveUserWallets(item: Boolean) { appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) } + override suspend fun useBiometricAuthentication(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + ) + + if (useBiometricAuthentication != null) { + return useBiometricAuthentication + } + + val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SAVE_USER_WALLETS_KEY, + ) + + if (legacySaveWalletsInTheApp != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + value = legacySaveWalletsInTheApp, + ) + return legacySaveWalletsInTheApp + } else { + // Default value for new users + setUseBiometricAuthentication(false) + return false + } + } + + override suspend fun setUseBiometricAuthentication(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value) + } + + override suspend fun requireAccessCode(): Boolean { + val requireAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + ) + + if (requireAccessCode != null) { + return requireAccessCode + } + + val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, + ) + + if (legacyShouldSaveAccessCode != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + value = legacyShouldSaveAccessCode.not(), + ) + return legacyShouldSaveAccessCode.not() + } else { + // Default value for new users + setRequireAccessCode(true) + return true + } + } + + override suspend fun setRequireAccessCode(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value) + } + override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean { return appPreferencesStore .getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index 9c26e8a3ee..b53cd193d1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -1,7 +1,11 @@ package com.tangem.data.wallets.hot import com.tangem.common.core.TangemSdkError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.copy import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* @@ -9,7 +13,9 @@ import javax.inject.Inject class HotWalletAccessor @Inject constructor( private val tangemHotSdk: TangemHotSdk, + private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletPasswordRequester: HotWalletPasswordRequester, + private val walletsRepository: WalletsRepository, ) { suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = @@ -23,10 +29,18 @@ class HotWalletAccessor @Inject constructor( } private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth HotWalletId.AuthType.Password -> requestPassword(false) - HotWalletId.AuthType.Biometry -> HotAuth.Biometry + HotWalletId.AuthType.Biometry -> { + if (isAccessCodeRequired) { + requestPassword(false) + } else { + HotAuth.Biometry + } + } } return runCatchingSdkErrors(hotWalletId, auth) { @@ -47,20 +61,41 @@ class HotWalletAccessor @Inject constructor( block = { blockAuth -> block(blockAuth).also { // Update biometry auth if the original auth was password - if (blockAuth is HotAuth.Password) { - tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = blockAuth, - ), - auth = HotAuth.Biometry, - ) - } + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = blockAuth, + ) } }, ) } + private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + + if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + val userWallet = userWalletsListRepository.userWalletsSync() + .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as? UserWallet.Hot + ?: return + + val newHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = originalAuth, + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet = userWallet.copy( + hotWalletId = newHotWalletId, + ), + canOverride = true, + ) + } + } + private suspend fun runCatchingWrongPassInternal( originalAuth: HotAuth, auth: HotAuth, diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt index f0f9ee988a..fbcfeb9a0c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -73,8 +73,21 @@ interface UserWalletsListRepository { * If the wallet is not found, it returns [SetLockError.UserWalletNotFound] * If the wallet is locked, it returns [SetLockError.UserWalletLocked] * If the lock method is not supported, it returns [SetLockError.UnableToSetLock]. + * + * @param userWalletId The ID of the user wallet to set the lock for. + * @param lockMethod The method to use for locking the wallet. + * @param changeUnsecured If false, the method will have no effect on unsecured wallets. */ - suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either + suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean = true, + ): Either + + /** + * Removes biometric lock for user wallet if it is set. + */ + suspend fun removeBiometricLock(userWalletId: UserWalletId) /** * Deletes user wallets by ids. diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 1a64989894..dd8608db95 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -24,8 +24,10 @@ interface SettingsRepository { suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun shouldSaveAccessCodes(): Boolean + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun setShouldSaveAccessCodes(value: Boolean) suspend fun incrementAppLaunchCounter() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..a1eb1f0cd8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -11,10 +11,20 @@ interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean + @Deprecated("Hot wallet make always save user wallets. Do not use this method") fun shouldSaveUserWallets(): Flow + @Deprecated("Hot wallet make always save user wallets. Do not use this method") suspend fun saveShouldSaveUserWallets(item: Boolean) + suspend fun useBiometricAuthentication(): Boolean + + suspend fun setUseBiometricAuthentication(value: Boolean) + + suspend fun requireAccessCode(): Boolean + + suspend fun setRequireAccessCode(value: Boolean) + suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 23b10bf3c9..7e328ae146 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -6,11 +6,12 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet @@ -22,6 +23,7 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository class SaveWalletUseCase( private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, private val useNewRepository: Boolean, ) { @@ -35,10 +37,14 @@ class SaveWalletUseCase( if (newUserWallet) { when (userWallet) { is UserWallet.Cold -> { - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } else { + Unit.right() + } } is UserWallet.Hot -> { userWalletsListRepository.setLock( diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index 4ee4b4358b..def97bf1f3 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { api(projects.features.biometry.api) + implementation(projects.features.hotWallet.api) /** Core modules */ implementation(projects.core.ui) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 0713f99e5b..614b462e24 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository @@ -20,6 +21,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -45,6 +47,8 @@ internal class AskBiometryModel @Inject constructor( private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor( walletsRepository.saveShouldSaveUserWallets(item = true) settingsRepository.setShouldSaveAccessCodes(value = true) - if (userWallet is UserWallet.Cold) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + walletsRepository.setUseBiometricAuthentication(value = true) + setBiometryLockForAllWallets() cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, + isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), ) + } else { + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + } } if (_uiState.value.bottomSheetVariant) { @@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor( params.modelCallbacks.onAllowed() } + private fun setBiometryLockForAllWallets() { + modelScope.launch { + userWalletsListRepository.userWalletsSync().forEach { userWallet -> + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + } + private fun showEnrollBiometricsDialog() { uiMessageSender.send( DialogMessage( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 373af1cf05..2db69f48ee 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk @@ -28,6 +29,7 @@ internal class AccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, private val tangemHotSdk: TangemHotSdk, ) : Model() { @@ -85,13 +87,15 @@ internal class AccessCodeModel @Inject constructor( auth = HotAuth.Password(accessCode.toCharArray()), ) - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = updatedHotWalletId, - auth = HotAuth.Password(accessCode.toCharArray()), - ), - auth = HotAuth.Biometry, - ) + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, + auth = HotAuth.Password(accessCode.toCharArray()), + ), + auth = HotAuth.Biometry, + ) + } userWalletsListRepository.saveWithoutLock( userWallet.copy( @@ -106,10 +110,12 @@ internal class AccessCodeModel @Inject constructor( UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), ) - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } params.callbacks.onAccessCodeConfirmed(params.userWalletId) }.onFailure { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 29196737e8..d495361f44 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -91,7 +91,7 @@ internal class AddExistingWalletImportModel @Inject constructor( val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() - saveUserWalletUseCase(userWallet) + saveUserWalletUseCase(userWallet.copy(backedUp = true)) params.callbacks.onWalletImported(userWallet.walletId) }.onFailure { Timber.e(it) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index b81a8806c5..0c6739540e 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.welcome.impl.R @@ -35,6 +36,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -43,6 +45,7 @@ internal class WelcomeModel @Inject constructor( private val userWalletsFetcherFactory: UserWalletsFetcher.Factory, private val userWalletsListRepository: UserWalletsListRepository, private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, + private val walletsRepository: WalletsRepository, ) : Model() { // TODO add intent handling @@ -154,8 +157,7 @@ internal class WelcomeModel @Inject constructor( when (option) { Create -> router.push(AppRoute.CreateWalletSelection) Add -> router.push(AppRoute.AddExistingWallet) - Buy -> { - } + Buy -> Unit // TODO } } @@ -182,8 +184,8 @@ internal class WelcomeModel @Inject constructor( unlockWallet(userWallet.walletId, unlockMethod) } - private fun canUnlockWithBiometrics(): Boolean { - return getIsBiometricsEnabledUseCase.canUseBiometry() + private suspend fun canUnlockWithBiometrics(): Boolean { + return getIsBiometricsEnabledUseCase.canUseBiometry() && walletsRepository.useBiometricAuthentication() } suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index b24c0a7b55..305b00750c 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -70,16 +70,18 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", - onClick = state.onUnlockWithBiometricClick, - ) + if (state.showUnlockWithBiometricButton) { + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = "Unlock all with biometric", + onClick = state.onUnlockWithBiometricClick, + ) + } } LaunchedEffect(state.wallets) { From 7cfd21d898438366f8db34f64e389d1950b1bb21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:24:55 +0300 Subject: [PATCH 121/165] Updated on 2026-08-14 --- .../features/welcome/impl/model/WelcomeModel.kt | 16 +++++++++++++--- .../tangem/features/welcome/impl/ui/Welcome.kt | 4 +++- .../features/welcome/impl/ui/state/WelcomeUM.kt | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 0c6739540e..1901cc427a 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -67,6 +67,7 @@ internal class WelcomeModel @Inject constructor( ) private val walletsFetcherJobHolder = JobHolder() private val wallets = MutableStateFlow>(persistentListOf()) + private var routedOut = false init { modelScope.launch { @@ -87,6 +88,7 @@ internal class WelcomeModel @Inject constructor( if (canUnlockWithBiometrics()) { userWalletsListRepository.unlockAllWallets() .onRight { + routedOut = true router.replaceAll(AppRoute.Wallet) } .onLeft { @@ -100,16 +102,19 @@ internal class WelcomeModel @Inject constructor( } } - private fun tryToUnlockWithAccessCodeRightAway() = modelScope.launch { + private suspend fun tryToUnlockWithAccessCodeRightAway() { if (onlyOneHotWalletWithAccessCode()) { val userWallets = userWalletsListRepository.userWalletsSync() val userWallet = userWallets.first() + uiState.value = WelcomeUM.Empty unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) } } private fun setSelectWalletState() { modelScope.launch { + if (routedOut || uiState.value is WelcomeUM.SelectWallet) return@launch + uiState.value = WelcomeUM.SelectWallet( wallets = walletsFetcher.userWallets.first(), showUnlockWithBiometricButton = canUnlockWithBiometrics(), @@ -178,10 +183,14 @@ internal class WelcomeModel @Inject constructor( val unlockMethod = when (userWallet) { is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan - is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode + is UserWallet.Hot -> { + uiState.value = WelcomeUM.Empty + UserWalletsListRepository.UnlockMethod.AccessCode + } } unlockWallet(userWallet.walletId, unlockMethod) + setSelectWalletState() } private suspend fun canUnlockWithBiometrics(): Boolean { @@ -191,6 +200,7 @@ internal class WelcomeModel @Inject constructor( suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { userWalletsListRepository.unlock(userWalletId, unlockMethod) .onRight { + routedOut = true userWalletsListRepository.select(userWalletId) router.replaceAll(AppRoute.Wallet) } @@ -199,7 +209,7 @@ internal class WelcomeModel @Inject constructor( } } - suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: () -> Unit = { }) { + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { when (this) { UnlockWalletError.AlreadyUnlocked -> { // this should not happen, as we check for locked state before this diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index 3427544768..04b1e81910 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -35,6 +35,7 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) + WelcomeUM.Empty -> {} } } } @@ -79,7 +80,8 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Empty + WelcomeUM.Empty -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 390446ba73..c86bd0befa 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -9,6 +9,8 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class WelcomeUM { + data object Empty : WelcomeUM() + data object Plain : WelcomeUM() data class SelectWallet( From 1c192d52b20c68b94ffd5b03b42b276dd5639e4a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:25:47 +0300 Subject: [PATCH 122/165] Updated on 2026-08-14 --- .../data/walletmanager/WalletManagerFactory.kt | 4 ++-- .../derivations/MissedDerivationsFinder.kt | 9 ++------- .../domain/wallets/config/ColdCurvesConfig.kt | 18 ++++++++++++++++++ .../domain/wallets/config/CurvesConfig.kt | 18 ++++++++++++++++++ .../domain/wallets/config/HotCurvesConfig.kt | 15 +++++++++++++++ .../wallets/extension/UserWalletExtensions.kt | 7 ++----- 6 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index 2ab20f0a42..edd6da8af5 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,10 +7,10 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber @@ -42,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = Wallet2CardConfig.primaryCurve(blockchain) + val curve = hotWallet.curvesConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index dffa80fd65..ce12ab7516 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -7,12 +7,11 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.operations.derivation.ExtendedPublicKeysMap import kotlin.collections.forEach @@ -51,13 +50,9 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } private fun List.mapToNewDerivations(): List { - val config = when (userWallet) { - is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card) - is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - } return mapNotNull { network -> val blockchain = network.toBlockchain() - val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null + val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null val walletPublicKey = when (userWallet) { is UserWallet.Cold -> { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt new file mode 100644 index 0000000000..103f85021a --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO + +class ColdCurvesConfig(cardDTO: CardDTO) : CurvesConfig { + + val cardConfig = CardConfig.createConfig(cardDTO) + + override val mandatoryCurves: List + get() = cardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return cardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt new file mode 100644 index 0000000000..dcf753e3e3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.models.wallet.UserWallet + +interface CurvesConfig { + + val mandatoryCurves: List + + fun primaryCurve(blockchain: Blockchain): EllipticCurve? +} + +val UserWallet.curvesConfig: CurvesConfig + get() = when (this) { + is UserWallet.Cold -> ColdCurvesConfig(this.scanResponse.card) + is UserWallet.Hot -> HotCurvesConfig + } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt new file mode 100644 index 0000000000..eec380f63d --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.Wallet2CardConfig + +data object HotCurvesConfig : CurvesConfig { + + override val mandatoryCurves: List + get() = Wallet2CardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return Wallet2CardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt index 25d25bd977..74055cd741 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt @@ -4,17 +4,14 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.common.util.hasDerivation -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.wallet.UserWallet -import kotlin.collections.first -import kotlin.collections.orEmpty +import com.tangem.domain.wallets.config.curvesConfig fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Boolean { return when (this) { is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath) is UserWallet.Hot -> { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) + val primaryCurve = curvesConfig.primaryCurve(blockchain) val list = if (blockchain == Blockchain.Cardano) { listOf( CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)), From c6aa63a44f95eec0fc16f9a5f1cb0ab992d19144 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 15:55:14 +0500 Subject: [PATCH 123/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 13 ++- .../com/tangem/common/routing/AppRoute.kt | 5 + .../hotwallet/CreateWalletBackupComponent.kt | 14 +++ .../CreateWalletBackupModel.kt | 97 +++++++++++++++++++ .../CreateWalletBackupStepperStateManager.kt | 56 +++++++++++ .../DefaultCreateWalletBackupComponent.kt | 92 ++++++++++++++++++ .../di/CreateWalletBackupModule.kt | 42 ++++++++ .../routing/CreateWalletBackupChildFactory.kt | 47 +++++++++ .../routing/CreateWalletBackupRoute.kt | 19 ++++ .../ui/CreateWalletBackupContent.kt | 42 ++++++++ .../walletbackup/model/WalletBackupModel.kt | 3 +- .../walletsettings/entity/WalletSettingsUM.kt | 1 + .../model/WalletSettingsModel.kt | 37 ++++++- 13 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 71f7c77d8b..ed41201e3f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -17,9 +17,10 @@ import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent -import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.CreateWalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -101,6 +102,7 @@ internal class ChildFactory @Inject constructor( private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val walletActivationComponentFactory: WalletActivationComponent.Factory, + private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, @@ -498,6 +500,15 @@ internal class ChildFactory @Inject constructor( componentFactory = walletActivationComponentFactory, ) } + is AppRoute.CreateWalletBackup -> { + createComponentChild( + context = context, + params = CreateWalletBackupComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = createWalletBackupComponentFactory, + ) + } is AppRoute.UpdateAccessCode -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index e0d6d80408..55cc7fca0d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -308,6 +308,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") + @Serializable + data class CreateWalletBackup( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") + @Serializable data class UpdateAccessCode( val userWalletId: UserWalletId, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt new file mode 100644 index 0000000000..84d8828d8f --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface CreateWalletBackupComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt new file mode 100644 index 0000000000..8cc687e476 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -0,0 +1,97 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateWalletBackupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onBack() { + when (currentRoute.value) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop() + is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop() + is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop() + is CreateWalletBackupRoute.BackupCompleted -> router.pop() + } + } + + fun onManualBackupStarted() { + stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase) + } + + fun onManualBackupPhraseShown() { + stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup) + } + + fun onManualBackupChecked() { + stackNavigation.push(CreateWalletBackupRoute.BackupCompleted) + } + + fun onManualBackupCompleted() { + router.pop() + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onBack() + } + + override fun onSkipClick() = Unit + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupStarted() + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupPhraseShown() + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + onManualBackupChecked() + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + onManualBackupCompleted() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt new file mode 100644 index 0000000000..5b8c3a2e68 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt @@ -0,0 +1,56 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import javax.inject.Inject + +internal class CreateWalletBackupStepperStateManager @Inject constructor() { + + fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_START, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_CONFIRM, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 4 + + private const val STEP_START = 1 + private const val STEP_PHRASE = 2 + private const val STEP_CONFIRM = 3 + private const val STEP_COMPLETED = 4 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt new file mode 100644 index 0000000000..b13c9193f9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory +import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultCreateWalletBackupComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: CreateWalletBackupComponent.Params, + private val stepperStateManager: CreateWalletBackupStepperStateManager, + createWalletBackupChildFactory: CreateWalletBackupChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : CreateWalletBackupComponent, AppComponentContext by appComponentContext { + + private val model: CreateWalletBackupModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "createWalletBackupInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createWalletBackupChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + CreateWalletBackupContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateWalletBackupComponent.Factory { + override fun create( + context: AppComponentContext, + params: CreateWalletBackupComponent.Params, + ): DefaultCreateWalletBackupComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt new file mode 100644 index 0000000000..5ea687703d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager +import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletBackupModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletBackupComponentFactory( + impl: DefaultCreateWalletBackupComponent.Factory, + ): CreateWalletBackupComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletBackupModel::class) + fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletBackupModule { + + @Provides + @Singleton + fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager { + return CreateWalletBackupStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt new file mode 100644 index 0000000000..44d740af6c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import javax.inject.Inject + +internal class CreateWalletBackupChildFactory @Inject constructor() { + + fun createChild( + route: CreateWalletBackupRoute, + childContext: AppComponentContext, + model: CreateWalletBackupModel, + ): ComposableContentComponent = when (route) { + CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt new file mode 100644 index 0000000000..f063a1f796 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt @@ -0,0 +1,19 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface CreateWalletBackupRoute { + + @Serializable + data object RecoveryPhraseStart : CreateWalletBackupRoute + + @Serializable + data object RecoveryPhrase : CreateWalletBackupRoute + + @Serializable + data object ConfirmBackup : CreateWalletBackupRoute + + @Serializable + data object BackupCompleted : CreateWalletBackupRoute +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt new file mode 100644 index 0000000000..e6f1e9e732 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun CreateWalletBackupContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 37c2dd7948..b398ed047a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -66,8 +67,8 @@ internal class WalletBackupModel @Inject constructor( secondaryButton { text = resourceReference(R.string.hw_backup_need_action) onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) closeBs() - // TODO [REDACTED_TASK_KEY] } } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 3cadad2268..39bd16ae56 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -9,4 +9,5 @@ internal data class WalletSettingsUM( val items: PersistentList, val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, + val isWalletBackedUp: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 22dd035507..f9d0b633b2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,10 +15,16 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -86,9 +92,29 @@ internal class WalletSettingsModel @Inject constructor( items = persistentListOf(), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, + isWalletBackedUp = true, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { combine( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), @@ -97,6 +123,10 @@ internal class WalletSettingsModel @Inject constructor( ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + val isWalletBackedUp = when (wallet) { + is UserWallet.Hot -> wallet.backedUp + is UserWallet.Cold -> true + } val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> @@ -110,6 +140,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), ), + isWalletBackedUp = isWalletBackedUp, ) } } @@ -330,6 +361,10 @@ internal class WalletSettingsModel @Inject constructor( } private fun onAccessCodeClick() { - router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + if (!state.value.isWalletBackedUp) { + messageSender.send(makeBackupAtFirstAlertBS) + } else { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } } } \ No newline at end of file From 1f1f7adef44f1904db8249ba25557a646c83ca56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:32:52 +0400 Subject: [PATCH 124/165] Updated on 2026-08-14 --- .../DefaultAccountsCRUDRepository.kt | 39 ++++- domain/account/build.gradle.kts | 2 + .../repository/AccountsCRUDRepository.kt | 23 +++ .../usecase/GetArchivedAccountsUseCase.kt | 86 ++++++++++ .../usecase/GetArchivedAccountsUseCaseTest.kt | 158 ++++++++++++++++++ 5 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a46b5d096a..a9fba30f57 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -13,6 +13,8 @@ import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** [REDACTED_AUTHOR] @@ -37,16 +39,23 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun getArchivedAccount(accountId: AccountId): Option = option { - ArchivedAccount( - accountId = accountId, - name = AccountName("Archived Account").getOrNull()!!, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = DerivationIndex(value = 1000).getOrNull()!!, - tokensCount = 2, - networksCount = 1, + createMockArchivedAccount(userWalletId = accountId.userWalletId) + } + + override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { + listOf( + createMockArchivedAccount(userWalletId), ) } + override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { + return flow { + getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() + } + } + + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + override suspend fun saveAccounts(accountList: AccountList) { runtimeStore.update(emptyList()) { it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } @@ -62,4 +71,20 @@ internal class DefaultAccountsCRUDRepository( override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } + + private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { + val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + tokensCount = 2, + networksCount = 1, + ) + } } \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index cf1bc96831..75db105c86 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -10,10 +10,12 @@ tasks.withType().configureEach { dependencies { + api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) implementation(deps.kotlin.serialization) testImplementation(deps.test.coroutine) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a05f267633..ac6921e167 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Repository interface for performing CRUD operations on accounts @@ -38,6 +39,28 @@ interface AccountsCRUDRepository { */ suspend fun getArchivedAccount(accountId: AccountId): Option + /** + * Retrieves a list of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not + */ + suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + + /** + * Provides a flow of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + fun getArchivedAccounts(userWalletId: UserWalletId): Flow> + + /** + * Fetches archived accounts for a specific user wallet and updates the repository + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + /** * Saves a list of accounts to the repository * diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt new file mode 100644 index 0000000000..cbcfb13168 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch + +typealias ArchivedAccountList = List + +/** + * Use case for retrieving archived accounts for a specific user wallet + * + * @property crudRepository the repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetArchivedAccountsUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Executes the use case to retrieve archived accounts for the given user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + operator fun invoke(userWalletId: UserWalletId): LceFlow = channelFlow { + val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId) + + archivedAccounts + .onRight { send(it.lceContent()) } + .onLeft { + send(lceLoading()) + + launch { + fetchArchivedAccounts(userWalletId).getOrElse { + send(it.lceError()) + } + } + } + + subscribeOnArchivedAccounts(userWalletId) + } + .distinctUntilChanged() + + private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { + crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + error("Archived accounts not found for user wallet: $userWalletId") + } + } + } + + private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) } + } + + private suspend fun ProducerScope>.subscribeOnArchivedAccounts( + userWalletId: UserWalletId, + ) { + crudRepository.getArchivedAccounts(userWalletId) + .distinctUntilChanged() + .retryWhen { cause, _ -> + send(cause.lceError()) + + delay(timeMillis = 2000) + + true + } + .collectLatest { archivedAccounts -> + send(archivedAccounts.lceContent()) + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt new file mode 100644 index 0000000000..eb0019f93c --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetArchivedAccountsUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetArchivedAccountsUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should emit archived accounts when repository returns data`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf(archivedAccounts.lceContent()) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + + coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) } + } + + @Test + fun `invoke should emit loading and fetch when accounts not found`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Fetch error") + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() + coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + exception.lceError(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun TestScope.getEmittedValues(flow: Flow): List { + val values = mutableListOf() + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) + } + + return values + } +} \ No newline at end of file From a443332da81a8c9cb9057b52ba2d3c78cedc1a10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 15:12:25 +0300 Subject: [PATCH 125/165] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index bc4cd43085..794a8187e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 +Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a From 40993944eefe9012d0ae80a6d8c3f6b16f472e0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 14:37:42 +0700 Subject: [PATCH 126/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 + .../com/tangem/common/routing/AppRoute.kt | 5 + core/res/src/main/res/values-de/strings.xml | 3 +- core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-uk-rUA/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 9 + .../account/ArchivedAccountListComponent.kt | 11 + .../archived/ArchivedAccountListModel.kt | 68 ++++++ .../DefaultArchivedAccountListComponent.kt | 40 ++++ .../archived/di/AccountArchivedModule.kt | 27 +++ .../archived/entity/AccountArchivedUM.kt | 27 +++ .../archived/ui/ArchivedAccountListContent.kt | 202 ++++++++++++++++++ .../details/ui/AccountDetailsContent.kt | 2 +- 14 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index ed41201e3f..d4b9a8ee88 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,6 +8,7 @@ import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent @@ -94,6 +95,7 @@ internal class ChildFactory @Inject constructor( private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory, private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory, private val accountDetailsComponentFactory: AccountDetailsComponent.Factory, + private val archivedAccountListComponentFactory: ArchivedAccountListComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, @@ -565,6 +567,15 @@ internal class ChildFactory @Inject constructor( componentFactory = accountDetailsComponentFactory, ) } + is AppRoute.ArchivedAccountList -> { + createComponentChild( + context = context, + params = ArchivedAccountListComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = archivedAccountListComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 55cc7fca0d..719d1dd05d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -346,4 +346,9 @@ sealed class AppRoute(val path: String) : Route { data class AccountDetails( val account: Account, ) : AppRoute(path = "/account_details/${account.accountId.value}") + + @Serializable + data class ArchivedAccountList( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 38278a60f0..c9acb51a42 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1408,6 +1408,7 @@ Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Tangem Wallet unterstützt derzeit nicht %s Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. @@ -1460,7 +1461,7 @@ Transaktionsanfrage Transaktionsanfrage Unbegrenzte Menge - Wallet verbinden + WalletConnect Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 900aa6723d..a24b04d401 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1355,6 +1355,7 @@ Demande de transaction Demande de transaction Montant illimité + WalletConnect Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index f9fdb1b3f2..6bef7c8991 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1359,7 +1359,7 @@ Нет сетей Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова Предложение подключения истекло - Предварительные изменения + Прогнозируемые изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s Предложено %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 952d5a7dd6..e8b9b18fbc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1302,6 +1302,7 @@ Сеанс Wallet Connect було завершено Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою + Tangem Wallet наразі не підтримує %s Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s @@ -1354,7 +1355,7 @@ До Запит транзакції Запит транзакції - Підключення гаманця + WalletConnect Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a76469f95b..f9238788d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -495,6 +495,7 @@ Stay up to date with the latest features and news Seed phrase backup Create Mobile Wallet + This recovery phrase has already been imported Mobile Wallet This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation @@ -1504,12 +1505,18 @@ Enable Trustline A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. Trustline Required + The required network %s is not added to your portfolio. Add it first, then proceed with the connection. + Add network to portfolio Malicious domain Unknown domain Connect anyway Timeout error. Please, try again later. Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. + To continue, please reconnect your dApp session with the required network %s. + Network not connected + Check your network connection + Request timeout Please return to your browser and reconnect via WalletConnect. Wallet Connect session was disconnected Sign anyway @@ -1520,6 +1527,8 @@ Unsupported dApp Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error + This network %s is not supported by Tangem Wallet and cannot be connected. + Unsupported network Tangem does not currently support a required network by %s. Unsupported networks This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s diff --git a/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt new file mode 100644 index 0000000000..91acb0ea4d --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface ArchivedAccountListComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt new file mode 100644 index 0000000000..3c2aed7f6b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -0,0 +1,68 @@ +package com.tangem.features.account.archived + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("UnusedPrivateMember") // todo account +internal class ArchivedAccountListModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun confirmRecoverDialog(accountId: AccountId) { + val account: Account? = null // todo account find + account ?: return + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_archived_recover), + onClick = { recoverCryptoPortfolio(account.accountId) }, + ) + messageSender.send( + DialogMessage( + title = stringReference(account.name.value), + message = TextReference.EMPTY, + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + recoverCryptoPortfolioUseCase(accountId) + } + + private fun getInitialState(): AccountArchivedUM { + return AccountArchivedUM.Loading( + onCloseClick = { router.pop() }, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt new file mode 100644 index 0000000000..6179fd12b2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.archived + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ui.ArchivedAccountListContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultArchivedAccountListComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: ArchivedAccountListComponent.Params, +) : AppComponentContext by appComponentContext, ArchivedAccountListComponent { + + private val model: ArchivedAccountListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ArchivedAccountListContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : ArchivedAccountListComponent.Factory { + override fun create( + context: AppComponentContext, + params: ArchivedAccountListComponent.Params, + ): DefaultArchivedAccountListComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt new file mode 100644 index 0000000000..21c674cef2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ArchivedAccountListModel +import com.tangem.features.account.archived.DefaultArchivedAccountListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountArchivedModule { + + @Binds + fun bindArchivedAccountListComponentFactory( + impl: DefaultArchivedAccountListComponent.Factory, + ): ArchivedAccountListComponent.Factory + + @Binds + @IntoMap + @ClassKey(ArchivedAccountListModel::class) + fun bindArchivedAccountListModel(model: ArchivedAccountListModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt new file mode 100644 index 0000000000..ee42b871ff --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.account.common.CryptoPortfolioIconUM +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface AccountArchivedUM { + val onCloseClick: () -> Unit + + data class Loading(override val onCloseClick: () -> Unit) : AccountArchivedUM + data class Error( + override val onCloseClick: () -> Unit, + val onRetryClick: () -> Unit, + ) : AccountArchivedUM + data class Content( + override val onCloseClick: () -> Unit, + val accounts: ImmutableList, + ) : AccountArchivedUM +} + +internal data class ArchivedAccountUM( + val accountId: String, + val accountName: String, + val accountIcon: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val onClick: (accountId: String) -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt new file mode 100644 index 0000000000..65b5e1794c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -0,0 +1,202 @@ +package com.tangem.features.account.archived.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.features.account.archived.entity.ArchivedAccountUM +import com.tangem.features.account.common.toUM +import com.tangem.features.account.details.ui.AccountIcon +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ArchivedAccountListContent(state: AccountArchivedUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.account_archived_title), + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + + ) { + when (state) { + is AccountArchivedUM.Content -> ArchiveAccountContent(state) + is AccountArchivedUM.Error -> ArchiveAccountError(state) + is AccountArchivedUM.Loading -> ArchiveAccountLoading() + } + } + } +} + +@Composable +private fun ArchiveAccountLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + modifier = Modifier, + ) + } +} + +@Composable +private fun ArchiveAccountError(state: AccountArchivedUM.Error, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = stringResourceSafe(R.string.common_unable_to_load), + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = state.onRetryClick, + ), + ) + } + } +} + +@Composable +private fun ArchiveAccountContent(state: AccountArchivedUM.Content, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = state.accounts, + key = { index, item -> item.accountId }, + ) { index, account -> + ArchivedAccountRow( + item = account, + modifier = Modifier.roundedShapeItemDecoration( + backgroundColor = TangemTheme.colors.background.primary, + radius = TangemTheme.dimens.radius20, + currentIndex = index, + addDefaultPadding = true, + lastIndex = state.accounts.lastIndex, + ), + ) + } + } +} + +@Composable +private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { item.onClick(item.accountId) }) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)), + accountName = item.accountName, + accountIcon = item.accountIcon, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = item.accountName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = item.tokensInfo.resolveReference(), + ) + } + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.account_archived_recover), + onClick = { item.onClick(item.accountId) }, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountArchivedUM) { + TangemThemePreview { + ArchivedAccountListContent(state = params) + } +} + +@Suppress("MagicNumber") +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + + val firstList = List(10) { + ArchivedAccountUM( + accountId = it.toString(), + accountName = "Account name", + accountIcon = portfolioIcon(), + tokensInfo = stringReference("10 tokens in 2 networks"), + onClick = {}, + + ) + }.toImmutableList() + val first = AccountArchivedUM.Content( + onCloseClick = {}, + accounts = firstList, + ) + add(first) + add(AccountArchivedUM.Loading {}) + add(AccountArchivedUM.Error({}, {})) + }, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt index 6234a1001b..08b3562176 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -183,7 +183,7 @@ private fun AccountRow(state: AccountDetailsUM) { // todo account make reusable @Composable -private fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { +internal fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, modifier = modifier.background(accountIcon.color.getUiColor()), From 28210080c5a06b11d8f9230e4c8c1b5179d98d5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 11:37:44 +0400 Subject: [PATCH 127/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 14441c2e44..fb79857d96 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-511" +tangemCardSdk = "develop-518" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 94196d74c42340e644a9518006484bc8e0615af4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:43:54 +0500 Subject: [PATCH 128/165] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++++ .../com/tangem/features/send/v2/api/SendFeatureToggles.kt | 1 + .../com/tangem/features/send/v2/DefaultSendFeatureToggles.kt | 2 ++ 3 files changed, 7 insertions(+) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6ecb2f5ac6..4cd82bc21e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -50,5 +50,9 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NFT_SEND_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index fb665394be..294674ae19 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -3,5 +3,6 @@ package com.tangem.features.send.v2.api interface SendFeatureToggles { val isSendRedesignEnabled: Boolean + val isNFTSendRedesignEnabled: Boolean val isSendWithSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index f414e52610..d73b45c69b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -8,6 +8,8 @@ internal class DefaultSendFeatureToggles( ) : SendFeatureToggles { override val isSendRedesignEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") + override val isNFTSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED") override val isSendWithSwapEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED") } \ No newline at end of file From f7bd99f737b690971c81ddadc4739bedff692d6c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:52:11 +0500 Subject: [PATCH 129/165] Updated on 2026-08-14 --- .../nft/component/NFTDetailsBlockComponent.kt | 5 +- .../block/DefaultNFTDetailsBlockComponent.kt | 2 + .../nft/details/block/ui/NFTDetailsBlock.kt | 31 +++-- .../success/ui/SendConfirmSuccessContent.kt | 2 +- .../v2/sendnft/DefaultNFTSendComponent.kt | 34 +++++- .../confirm/NFTSendConfirmComponent.kt | 39 ++++++- .../confirm/model/NFTSendConfirmModel.kt | 98 ++++++++++------ .../confirm/ui/NFTSendConfirmContent.kt | 59 ++++++---- .../send/v2/sendnft/di/NFTSendModelModule.kt | 6 + .../send/v2/sendnft/model/NFTSendModel.kt | 17 ++- .../success/NFTSendSuccessComponent.kt | 96 ++++++++++++++++ .../success/model/NFTSendSuccessModel.kt | 107 ++++++++++++++++++ .../success/ui/NFTSendSuccessContent.kt | 103 +++++++++++++++++ .../send/v2/sendnft/ui/state/NFTSendUM.kt | 5 +- 14 files changed, 527 insertions(+), 77 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt index ed1df0a1f2..801c8f6420 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt @@ -2,8 +2,9 @@ package com.tangem.features.nft.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.nft.models.NFTAsset interface NFTDetailsBlockComponent : ComposableContentComponent { @@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent { val userWalletId: UserWalletId, val nftAsset: NFTAsset, val nftCollectionName: String, + val title: TextReference, + val isSuccessScreen: Boolean, ) interface Factory : ComponentFactory diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt index 234cb0adfb..4856ca5634 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt @@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor( assetName = stringReference(params.nftAsset.name.orEmpty()), collectionName = stringReference(params.nftCollectionName), assetImage = params.nftAsset.media?.imageUrl, + title = params.title, + isSuccessScreen = params.isSuccessScreen, networkIconRes = getActiveIconRes(params.nftAsset.network.rawId), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt index 6336d2dbb2..8d7d1148c9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.common.ui.NFTLogo import com.tangem.features.nft.impl.R +@Suppress("LongParameterList") @Composable internal fun NFTDetailsBlock( + title: TextReference, assetName: TextReference, collectionName: TextReference, assetImage: String?, networkIconRes: Int, + isSuccessScreen: Boolean, ) { Column( modifier = Modifier @@ -35,20 +39,21 @@ internal fun NFTDetailsBlock( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = "NFT Asset", + text = title.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - NFTLogo( - assetImage, - networkIconRes, - background = TangemTheme.colors.background.action, - ) - + if (isSuccessScreen) { + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { @@ -63,6 +68,14 @@ internal fun NFTDetailsBlock( color = TangemTheme.colors.text.tertiary, ) } + if (!isSuccessScreen) { + SpacerWMax() + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } } } } @@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() { collectionName = stringReference("NFT Collection"), assetImage = null, networkIconRes = R.drawable.img_polygon_22, + title = stringReference("From My Wallet"), + isSuccessScreen = false, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 636828e73f..1d679a3e59 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -56,7 +56,7 @@ internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent .padding(horizontal = TangemTheme.dimens.spacing16) .scrollable( state = rememberScrollState(), - orientation = Orientation.Horizontal, + orientation = Orientation.Vertical, ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index c4ecdee6e2..b23440cfeb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -18,7 +18,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -29,6 +28,7 @@ import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams @@ -42,7 +42,8 @@ import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, - private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory, + private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory, private val analyticsEventHandler: AnalyticsEventHandler, ) : NFTSendComponent, AppComponentContext by appComponentContext { @@ -121,6 +122,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( is CommonSendRoute.Destination -> getDestinationComponent(factoryContext) is CommonSendRoute.Fee -> getFeeComponent(factoryContext) CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext) else -> getStubComponent() } @@ -164,9 +166,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( } } - private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create( appComponentContext = factoryContext, - nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, params = NFTSendConfirmComponent.Params( state = model.uiState.value, analyticsCategoryName = analyticsCategoryName, @@ -180,9 +181,34 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( currentRoute = model.currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, onLoadFee = model::loadFee, + onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) }, ), ) + private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl + + if (txUrl == null) { + model.showAlertError() + return getStubComponent() + } + + return nftSendSuccessComponentFactory.create( + appComponentContext = factoryContext, + params = NFTSendSuccessComponent.Params( + nftSendUMFlow = model.uiState, + analyticsCategoryName = analyticsCategoryName, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + callback = model, + currentRoute = model.currentRouteFlow.filterIsInstance(), + txUrl = txUrl, + ), + ) + } + private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index cc0fe9a936..d2fd5670b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -10,17 +10,23 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM @@ -28,13 +34,17 @@ import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinat import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* import java.math.BigDecimal -internal class NFTSendConfirmComponent( - appComponentContext: AppComponentContext, - params: Params, +internal class NFTSendConfirmComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: NFTSendConfirmModel = getOrCreateModel(params = params) @@ -74,12 +84,28 @@ internal class NFTSendConfirmComponent( onClick = model::showEditFee, ) + private val feeSelectorBlockComponent = feeSelectorComponentFactory.create( + context = child("NFTSendConfirmFeeSelectorBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, + ), + onResult = model::onFeeResult, + ) + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( context = child("NFTDetailsBlock"), params = NFTDetailsBlockComponent.Params( userWalletId = params.userWallet.walletId, nftAsset = params.nftAsset, nftCollectionName = params.nftCollectionName, + isSuccessScreen = false, + title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)), ), ) @@ -126,6 +152,7 @@ internal class NFTSendConfirmComponent( nftSendUM = state, destinationBlockComponent = destinationBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, notificationsComponent = notificationsComponent, notificationsUM = notificationState, @@ -145,9 +172,15 @@ internal class NFTSendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val onLoadFee: suspend () -> Either, + val onSendTransaction: () -> Unit, ) interface ModelCallback { fun onResult(nftSendUM: NFTSendUM) } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendConfirmComponent + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 522470a90e..d834d13885 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -33,6 +33,7 @@ import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger @@ -61,6 +62,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @Suppress("LongParameterList", "LargeClass") @ModelScoped @@ -89,7 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val sendFeeReloadTrigger: SendFeeReloadTrigger, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, -) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback { +) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() @@ -141,6 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor( updateConfirmNotifications() } + override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } updateConfirmNotifications() @@ -400,13 +408,23 @@ internal class NFTSendConfirmModel @Inject constructor( ).onEach { (state, _) -> val confirmUM = state.confirmUM val confirmUMContent = confirmUM as? ConfirmUM.Content - val isReadyToSend = confirmUMContent != null && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( title = resourceReference(R.string.nft_send), - subtitle = confirmUMContent?.walletName, - backIconRes = R.drawable.ic_close_24, + subtitle = if (uiState.value.isRedesignEnabled) { + null + } else { + confirmUMContent?.walletName + }, + backIconRes = if (state.isRedesignEnabled) { + when (confirmUM) { + is ConfirmUM.Success -> R.drawable.ic_close_24 + else -> R.drawable.ic_back_24 + } + } else { + R.drawable.ic_close_24 + }, backIconClick = { analyticsEventHandler.send( CommonSendAnalyticEvents.CloseButtonClicked( @@ -416,26 +434,13 @@ internal class NFTSendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, - primaryButton = NavigationButton( - textReference = when (confirmUM) { - is ConfirmUM.Success -> resourceReference(R.string.common_close) - is ConfirmUM.Content -> if (confirmUM.isSending) { - resourceReference(R.string.send_sending) - } else { - resourceReference(R.string.common_send) - } - else -> resourceReference(R.string.common_send) - }, - iconRes = R.drawable.ic_tangem_24, - isIconVisible = isReadyToSend, - isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = isReadyToSend, - onClick = { - onNextClick(confirmUM) - }, - ), + primaryButton = primaryButtonUM(), prevButton = null, secondaryPairButtonsUM = ( NavigationButton( @@ -454,21 +459,40 @@ internal class NFTSendConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun onNextClick(confirmUM: ConfirmUM) { - when (confirmUM) { - is ConfirmUM.Success -> { - modelScope.launch { - nftSendSuccessTrigger.triggerSuccessNFTSend() + private fun primaryButtonUM(): NavigationButton { + val confirmUM = uiState.value.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + return NavigationButton( + textReference = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) } - appRouter.pop() - } - is ConfirmUM.Content -> if (confirmUM.isSending) { - return - } else { - onSendClick() - } - else -> return - } + else -> resourceReference(R.string.common_send) + }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = isReadyToSend, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> { + modelScope.launch { + nftSendSuccessTrigger.triggerSuccessNFTSend() + } + appRouter.pop() + } + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ) } private companion object { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index c2a2de5774..9ff84edfb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.sendnft.confirm.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM @@ -21,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -40,6 +43,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { @@ -54,6 +58,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent = destinationBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { tapHelp(isDisplay = confirmUM.showTapHelp) @@ -79,31 +84,45 @@ private fun LazyListScope.blocks( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - AnimatedVisibility( - visible = nftSendUM.confirmUM is ConfirmUM.Success, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) { - val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } - TransactionDoneTitle( - title = resourceReference(R.string.sent_transaction_sent_title), - subtitle = resourceReference( - R.string.send_date_format, - wrappedList( - wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), - wrappedConfirmUM.transactionDate.toTimeFormat(), - ), - ), - modifier = Modifier.padding(vertical = 12.dp), + if (nftSendUM.isRedesignEnabled) { + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) + } else { + TransactionDoneTitleAnimated(nftSendUM = nftSendUM) + destinationBlockComponent.Content(modifier = Modifier) + nftDetailsBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) } - destinationBlockComponent.Content(modifier = Modifier) - - nftDetailsBlockComponent.Content(modifier = Modifier) - - feeBlockComponent.Content(modifier = Modifier) } } +} + +@Composable +private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = 12.dp), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index 3252c7bc71..5980fa7d22 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +24,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendConfirmModel::class) fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendSuccessModel::class) + fun provideNFTSendSuccessModel(model: NFTSendSuccessModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 0bebb6f107..90a8753b4a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -29,6 +29,8 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute @@ -36,6 +38,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -70,7 +73,8 @@ internal class NFTSendModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val alertFactory: SendConfirmAlertFactory, -) : Model(), SendNFTComponentCallback { + private val sendFeatureToggles: SendFeatureToggles, +) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -124,7 +128,7 @@ internal class NFTSendModel @Inject constructor( } else { when (currentRouteFlow.value) { is Destination -> router.push(Confirm) - Confirm -> router.push(ConfirmSuccess) + Confirm -> router.replaceAll(ConfirmSuccess) else -> onBackClick() } } @@ -193,6 +197,13 @@ internal class NFTSendModel @Inject constructor( } } + fun showAlertError() { + alertFactory.getGenericErrorState( + onFailedTxEmailClick = ::onFailedTxEmailClick, + popBack = router::pop, + ) + } + private fun onFailedTxEmailClick(errorMessage: String? = null) { saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -249,7 +260,9 @@ internal class NFTSendModel @Inject constructor( private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeUM = FeeUM.Empty(), + feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, + isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt new file mode 100644 index 0000000000..76f7329658 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.v2.sendnft.success + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel +import com.tangem.features.send.v2.sendnft.success.ui.NFTSendSuccessContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class NFTSendSuccessComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendSuccessModel = getOrCreateModel(params = params) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsSuccessBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + isSuccessScreen = true, + title = resourceReference(R.string.nft_asset), + ), + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponentFactory.create( + context = child("NFTDestinationSuccessBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = MutableStateFlow(false), + predefinedValues = PredefinedValues.Empty, + ), + onResult = {}, + onClick = {}, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + NFTSendSuccessContent( + nftSendUM = state, + destinationBlockComponent = sendDestinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + modifier = modifier, + ) + } + + data class Params( + val nftSendUMFlow: StateFlow, + val analyticsCategoryName: String, + val currentRoute: Flow, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val userWallet: UserWallet, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val txUrl: String, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendSuccessComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt new file mode 100644 index 0000000000..9352ec6898 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt @@ -0,0 +1,107 @@ +package com.tangem.features.send.v2.sendnft.success.model + +import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@Stable +@ModelScoped +internal class NFTSendSuccessModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, +) : Model() { + private val params: NFTSendSuccessComponent.Params = paramsContainer.require() + + val uiState = params.nftSendUMFlow + + init { + configConfirmSuccessNavigation() + } + + private fun configConfirmSuccessNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) -> + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = stringReference(""), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = true, + ), + ) + appRouter.pop() + }, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_close), + iconRes = null, + isEnabled = true, + isHapticClick = false, + onClick = { + appRouter.pop() + }, + ), + prevButton = null, + secondaryPairButtonsUM = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + onClick = ::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + onClick = ::onShareClick, + ), + ), + ), + ) + }.launchIn(modelScope) + } + + private fun onExploreClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName)) + urlOpener.openUrl(params.txUrl) + } + + private fun onShareClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName)) + shareManager.shareText(params.txUrl) + } + + interface ModelCallback { + fun onResult(sendUM: SendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt new file mode 100644 index 0000000000..10fa100bc9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.sendnft.success.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toPx +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import kotlinx.coroutines.delay + +@Composable +internal fun NFTSendSuccessContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + modifier: Modifier = Modifier, +) { + var visible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(ANIMATION_DELAY) + visible = true + } + + val height = ANIMATION_OFFSET.toPx().toInt() + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { height }, + ).plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate success content", + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Vertical, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (nftSendUM.confirmUM is ConfirmUM.Success) { + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + nftSendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + nftSendUM.confirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) + } + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = nftSendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } + } +} + +private const val ANIMATION_DELAY = 600L +private val ANIMATION_OFFSET = (-40).dp \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index fb48eed197..45287e3c6e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,13 +1,16 @@ package com.tangem.features.send.v2.sendnft.ui.state import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class NFTSendUM( val destinationUM: DestinationUM, val feeUM: FeeUM, + val feeSelectorUM: FeeSelectorUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, + val isRedesignEnabled: Boolean, ) \ No newline at end of file From eb0738872909b9406c66d5ae22aea6781b6c2615 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 11:40:46 +0400 Subject: [PATCH 130/165] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 60 +++++++------------ .../tangemTech/models/UserTokensResponse.kt | 1 + .../account/GetWalletAccountsResponse.kt | 23 +++++++ .../GetWalletArchivedAccountsResponse.kt | 9 +++ .../account/SaveWalletAccountsResponse.kt | 9 +++ .../models/account/WalletAccountDTO.kt | 17 ++++++ 6 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 14ec9f906f..51fd1ce19a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -1,9 +1,11 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.local.config.providers.models.ProviderModel import retrofit2.http.* @@ -30,9 +32,6 @@ interface TangemTechApi { @Query("limit") limit: Int? = null, ): ApiResponse - @GET("v1/rates") - suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse - @GET("v1/currencies") suspend fun getCurrencyList( @Header("Cache-Control") cacheControl: String = "max-age=600", @@ -68,46 +67,11 @@ interface TangemTechApi { @Query("fields") fields: String, ): ApiResponse - @GET("v1/promotion") - suspend fun getPromotionInfo( - @Query("programName") name: String, - @Header("Cache-Control") cacheControl: String = "max-age=600", - ): ApiResponse - - @GET("v1/settings/{wallet_id}") - suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("v1/settings/{wallet_id}") - suspend fun saveUserTokensSettings( - @Path("wallet_id") walletId: String, - @Body userTokensSettings: UserTokensSettingsResponse, - ): ApiResponse - @POST("v1/user-network-account") suspend fun createUserNetworkAccount( @Body body: CreateUserNetworkAccountBody, ): ApiResponse - @POST("v1/account") - suspend fun createUserTokensAccount( - @Body body: CreateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}") - suspend fun updateUserTokensAccount( - @Path("account_id") accountId: Int, - @Body body: UpdateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}/archive") - suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @PUT("v1/account/{account_id}/unarchive") - suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @GET("v1/features") - suspend fun getFeatures(): ApiResponse - @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) @GET("v1/networks/providers") suspend fun getBlockchainProviders(): Map> @@ -160,7 +124,7 @@ interface TangemTechApi { suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // endregion - // region wallets + // region user-wallets @PATCH("v1/user-wallets/wallets/{wallet_id}") suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse @@ -176,4 +140,20 @@ interface TangemTechApi { @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion + + // region account + @GET("/v1/wallets/{walletId}/accounts") + suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse + + @PUT("/v1/wallets/{walletId}/accounts") + suspend fun saveWalletAccounts( + @Path("walletId") walletId: String, + @Header("If-Match") ifMatch: String, + ): ApiResponse + + @GET("/v1/wallets/{walletId}/accounts/archived") + suspend fun getWalletArchivedAccounts( + @Path("walletId") walletId: String, + ): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 2476bf0a31..8e1dec299f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -16,6 +16,7 @@ data class UserTokensResponse( @JsonClass(generateAdapter = true) data class Token( @Json(name = "id") val id: String? = null, + @Json(name = "accountId") val accountId: String? = null, @Json(name = "networkId") val networkId: String, @Json(name = "derivationPath") val derivationPath: String? = null, @Json(name = "name") val name: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt new file mode 100644 index 0000000000..6c3afd812f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType + +@JsonClass(generateAdapter = true) +data class GetWalletAccountsResponse( + @Json(name = "wallet") val wallet: Wallet, + @Json(name = "accounts") val accounts: List, + @Json(name = "unassignedTokens") val unassignedTokens: List, +) { + + @JsonClass(generateAdapter = true) + data class Wallet( + @Json(name = "version") val version: Int, + @Json(name = "group") val group: GroupType, + @Json(name = "sort") val sort: SortType, + @Json(name = "totalAccounts") val totalAccounts: Int, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt new file mode 100644 index 0000000000..3f1db4c76b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetWalletArchivedAccountsResponse( + @Json(name = "archivedAccounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt new file mode 100644 index 0000000000..3f36276519 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SaveWalletAccountsResponse( + @Json(name = "accounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt new file mode 100644 index 0000000000..343b6cdf2a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +@JsonClass(generateAdapter = true) +data class WalletAccountDTO( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "derivation") val derivationIndex: Int, + @Json(name = "icon") val icon: String, + @Json(name = "iconColor") val iconColor: String, + @Json(name = "tokens") val tokens: List? = null, + @Json(name = "totalTokens") val totalTokens: Int? = null, + @Json(name = "totalNetworks") val totalNetworks: Int? = null, +) \ No newline at end of file From 3f6bcd3a0e80edafa5a59dd73894a083e5ef2c24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 11:43:43 +0400 Subject: [PATCH 131/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 +- .../sdk/impl/DefaultTangemSdkManager.kt | 11 ++-- .../domain/sdk/impl/MockTangemSdkManager.kt | 4 +- .../visa/VisaCustomerWalletApproveTask.kt | 33 +++++++----- .../tangem/datasource/api/pay/TangemPayApi.kt | 22 ++++---- .../GenerateNonceByCustomerWalletRequest.kt | 10 ++++ .../GetTokenByCustomerWalletRequest.kt | 12 +++++ data/visa/build.gradle.kts | 1 + .../tangem/data/pay/DefaultKycRepository.kt | 43 ++++++++++------ .../visa/DefaultVisaActivationRepository.kt | 20 ++++---- .../data/visa/DefaultVisaAuthRepository.kt | 33 ++++++++++++ .../model/VisaDataToSignByCustomerWallet.kt | 2 +- .../VisaSignedChallengeByCustomerWallet.kt | 6 +++ .../domain/pay/repository/KycRepository.kt | 5 +- .../visa/repository/VisaAuthRepository.kt | 10 ++++ .../com/tangem/features/kyc/KycComponent.kt | 7 ++- features/kyc/impl/build.gradle.kts | 3 +- .../features/kyc/DefaultKycComponent.kt | 50 +++++++------------ .../tangem/features/kyc/DefaultKycModel.kt | 32 ++++++++++++ .../tangem/features/kyc/di/FeatureModule.kt | 14 ++++++ features/wallet/impl/build.gradle.kts | 1 + .../com/tangem/sdk/api/TangemSdkManager.kt | 4 +- settings.gradle.kts | 2 +- 23 files changed, 226 insertions(+), 101 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt create mode 100644 features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31cb895042..b22a62d437 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -227,8 +227,8 @@ dependencies { implementation(projects.features.usedesk.impl) implementation(projects.features.hotWallet.api) implementation(projects.features.hotWallet.impl) + implementation(projects.features.kyc.api) //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) // implementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index aaf4f87888..09a9039974 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -24,9 +24,7 @@ import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager( ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = VisaCustomerWalletApproveTask( - visaDataForApprove = visaDataForApprove, + VisaCustomerWalletApproveTask.Input( + cardId = visaDataForApprove.customerWalletCardId, + targetAddress = visaDataForApprove.targetAddress, + hashToSign = visaDataForApprove.dataToSign.hashToSign, + sign = visaDataForApprove.dataToSign::sign, + ), ), cardId = visaDataForApprove.customerWalletCardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index edca4bea1a..1568e7dfc8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index a44a04e051..4fa773c18c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -10,7 +11,6 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError @@ -22,15 +22,13 @@ import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError -import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet -import com.tangem.domain.visa.model.sign import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand class VisaCustomerWalletApproveTask( - private val visaDataForApprove: VisaDataForApprove, + private val visaDataForApprove: Input, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { @@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask( return } - if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { + if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) { callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } @@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask( ) } + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK + private fun hashPersonalMessage(message: ByteArray): ByteArray { + val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() + return (prefix + message).toKeccak() + } + private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, @@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask( session: CardSession, callback: CompletionCallback, ) { - val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes() + val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}" + val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) val signTask = SignHashCommand( - hash = hashToSign, + hash = hash, walletPublicKey = targetWalletPublicKey, derivationPath = derivationPath, ) @@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask( is CompletionResult.Success -> { val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = result.data.signature, - hash = hashToSign, + hash = hash, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString().lowercase() @@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask( scanCard( session = session, callback = callback, - signedData = visaDataForApprove.dataToSign.sign( - signature = rsvSignature, - customerWalletAddress = visaDataForApprove.targetAddress, - ), + signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress), ) } is CompletionResult.Failure -> { @@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask( } } } + + data class Input( + val cardId: String? = null, + val targetAddress: String, + val hashToSign: String, + val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 2749826753..8678e1959c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -1,19 +1,7 @@ package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest -import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.* import retrofit2.http.Body import retrofit2.http.GET @@ -33,9 +21,17 @@ interface TangemPayApi { @Body request: GenerateNoneByCardWalletRequest, ): ApiResponse + @POST("v1/auth/challenge") + suspend fun generateNonceByCustomerWallet( + @Body request: GenerateNonceByCustomerWalletRequest, + ): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + @POST("v1/auth/token") + suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt new file mode 100644 index 0000000000..edd8260545 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNonceByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "customer_wallet_address") val customerWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt new file mode 100644 index 0000000000..9a5e47e327 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetTokenByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "session_id") val sessionId: String, + @Json(name = "signature") val signature: String, + @Json(name = "message_format") val messageFormat: String, +) \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ff1d5fb9ef..38d1d2957a 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(projects.libs.tangemSdkApi) /** DI */ implementation(deps.hilt.core) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt index e42238404f..cc1a69895e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt @@ -2,39 +2,52 @@ package com.tangem.data.pay import arrow.core.Either import com.squareup.moshi.Moshi +import com.tangem.common.map import com.tangem.core.error.UniversalError import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.sdk.api.TangemSdkManager import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.withContext -@Suppress("UnusedPrivateMember") class DefaultKycRepository @AssistedInject constructor( - @Assisted userWalletId: UserWalletId, @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, - private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaAuthRepository: VisaAuthRepository, + private val tangemSdkManager: TangemSdkManager, ) : KycRepository { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) - override suspend fun getKycStartInfo(): Either = withContext(dispatcherProvider.io) { - val authTokenForSpecificWallet = "get from userWalletId" - - request { - tangemPayApi.getKycAccess( - authHeader = authTokenForSpecificWallet, - ).getOrThrow().result + override suspend fun getKycStartInfo(address: String, cardId: String): Either { + var authHeader = "" + visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result -> + tangemSdkManager.visaCustomerWalletApprove( + VisaDataForApprove( + customerWalletCardId = cardId, + targetAddress = address, + dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge), + ), + ).map { signResult -> + visaAuthRepository.getTokenWithCustomerWallet( + sessionId = result.session.sessionId, + signature = signResult.signature, + nonce = signResult.dataToSign.hashToSign, + ).getOrNull()?.let { authHeader = it } + } + } + return request { + authHeader.ifEmpty { error("Cannot get auth header for KYC") } + tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result }.map { KycStartInfo( token = it.token, @@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor( @AssistedFactory interface Factory : KycRepository.Factory { - override fun create(userWalletId: UserWalletId): DefaultKycRepository + override fun create(): DefaultKycRepository } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 30457ea047..b38b9a82a8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - visaApi.activateByCustomerWallet( - authHeader = authTokens.getAuthHeader(), - body = ActivationByCustomerWalletRequest( - orderId = signedData.dataToSign.request.orderId, - customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( - deployAcceptanceSignature = signedData.signature, - customerWalletAddress = signedData.customerWalletAddress, + signedData.dataToSign.request?.orderId?.let { orderId -> + visaApi.activateByCustomerWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCustomerWalletRequest( + orderId = orderId, + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + deployAcceptanceSignature = signedData.signature, + customerWalletAddress = signedData.customerWalletAddress, + ), ), - ), - ).getOrThrow() + ).getOrThrow() + } ?: error("Order Id cannot be null") } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index c9c07624e5..8b3024f1fe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor( } } + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.generateNonceByCustomerWallet( + GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.getTokenByCustomerWallet( + GetTokenByCustomerWalletRequest( + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + "Bearer ${response.result.accessToken}" + } + } + override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt index 17de705186..d6c97a1919 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt @@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable @Serializable data class VisaDataToSignByCustomerWallet( - val request: VisaCustomerWalletDataToSignRequest, val hashToSign: String, + val request: VisaCustomerWalletDataToSignRequest? = null, ) fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) = diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt new file mode 100644 index 0000000000..7c46288fc1 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaSignedChallengeByCustomerWallet( + val challenge: String, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index e072914e92..7d46ff2d52 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.pay.KycStartInfo -import com.tangem.domain.models.wallet.UserWalletId interface KycRepository { - suspend fun getKycStartInfo(): Either + suspend fun getKycStartInfo(address: String, cardId: String): Either interface Factory { - fun create(userWalletId: UserWalletId): KycRepository + fun create(): KycRepository } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index f7f19ca0e9..098ca44c00 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -18,6 +18,16 @@ interface VisaAuthRepository { cardWalletAddress: String, ): Either + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt index 6a18888000..de05f5dc62 100644 --- a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -4,9 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext interface KycComponent { - fun launch() + fun launch(params: Params) interface Factory { fun create(appComponentContext: AppComponentContext): KycComponent } + + data class Params( + val targetAddress: String, + val cardId: String, + ) } \ No newline at end of file diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 3e07cc6bd5..1bf8fc66f2 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -13,8 +13,7 @@ android { dependencies { /** Api */ - //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) + implementation(projects.features.kyc.api) /** Domain */ implementation(projects.domain.visa) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index afabc4e184..92d39cee70 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -1,57 +1,41 @@ package com.tangem.features.kyc import com.sumsub.sns.core.SNSMobileSDK -import com.sumsub.sns.core.data.listener.SNSCompleteHandler import com.sumsub.sns.core.data.listener.TokenExpirationHandler -import com.sumsub.sns.core.data.model.SNSCompletionResult -import com.sumsub.sns.core.data.model.SNSInitConfig -import com.sumsub.sns.core.data.model.SNSSDKState import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.pay.repository.KycRepository -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.kyc.theme.TangemSNSTheme +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.kyc.theme.TangemSNSIconHandler +import com.tangem.features.kyc.theme.TangemSNSTheme import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.Locale class DefaultKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - private val kycRepositoryFactory: KycRepository.Factory, ) : KycComponent, AppComponentContext by appComponentContext { - private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF")) + private val model: DefaultKycModel = getOrCreateModel() - override fun launch() { + override fun launch(params: KycComponent.Params) { componentScope.launch { - val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch - - val tokenExpirationHandler = object : TokenExpirationHandler { - override fun onTokenExpired(): String? { - val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token } - return newToken + model.uiState.collect { + it?.let { startInfo -> + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired() = "" + } + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) + .withTheme(TangemSNSTheme.theme(activity)) + .withIconHandler(TangemSNSIconHandler()) + .withLocale(Locale("en")) + .build() + snsSdk.launch() } } - - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) - .withConf(SNSInitConfig(strings = mapOf())) - .withTheme(TangemSNSTheme.theme(activity)) - .withIconHandler(TangemSNSIconHandler()) - .withLocale(Locale("en")) - .withCompleteHandler( - object : SNSCompleteHandler { - override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) { - } - }, - ) - .build() - - snsSdk.launch() } + model.getKycToken(params) } @AssistedFactory diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt new file mode 100644 index 0000000000..aff81ad334 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -0,0 +1,32 @@ +package com.tangem.features.kyc + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +class DefaultKycModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + kycRepositoryFactory: KycRepository.Factory, +) : Model() { + + private val kycRepository = kycRepositoryFactory.create() + + private val _uiState: MutableStateFlow = MutableStateFlow(null) + val uiState = _uiState.asStateFlow() + + fun getKycToken(params: KycComponent.Params) { + modelScope.launch { + kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull() + ?.let { _uiState.emit(it) } + } + } +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt index c72f282775..a2fefafc7b 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt @@ -1,11 +1,16 @@ package com.tangem.features.kyc.di +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model import com.tangem.features.kyc.DefaultKycComponent +import com.tangem.features.kyc.DefaultKycModel import com.tangem.features.kyc.KycComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) @@ -13,4 +18,13 @@ internal interface FeatureModule { @Binds fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(DefaultKycModel::class) + fun provideModel(model: DefaultKycModel): Model } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e6d1f16109..5d506674c2 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -113,6 +113,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.kyc.api) /** Common modules */ implementation(projects.common) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index f15198c725..12aeb18278 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -15,10 +15,8 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/settings.gradle.kts b/settings.gradle.kts index 2e4a4b6f20..b77ede786a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -260,8 +260,8 @@ include(":features:walletconnect:impl") include(":features:hot-wallet:api") include(":features:hot-wallet:impl") +include(":features:kyc:api") //TODO disable for release because of the permissions -// include(":features:kyc:api") // include(":features:kyc:impl") include(":features:create-wallet-selection:api") From dc3d109bf8160fbfc76b040695cebd95b8cf3e37 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 09:19:21 +0400 Subject: [PATCH 132/165] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 45 ++-------------- .../domain/tokens/GetTokenListUseCase.kt | 4 +- .../tokens/GetWalletTotalBalanceUseCase.kt | 4 +- .../BaseCurrencyStatusOperations.kt | 4 ++ .../CachedCurrenciesStatusesOperations.kt | 53 ++++++++++++------- 5 files changed, 45 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 089dc2bf5d..4a0a3c5922 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -18,7 +18,6 @@ import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -97,11 +96,11 @@ internal object TokensDomainModule { @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetTokenListUseCase { return GetTokenListUseCase( currenciesRepository = currenciesRepository, - currenciesStatusesOperations = baseCurrenciesStatusesOperations, + currenciesStatusesOperations = currenciesStatusesOperations, ) } @@ -369,9 +368,9 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetWalletTotalBalanceUseCase( - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) + return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) } @Provides @@ -399,42 +398,6 @@ internal object TokensDomainModule { return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) } - @Provides - @Singleton - fun provideBaseCurrenciesStatusesOperations( - tokensFeatureToggles: TokensFeatureToggles, - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - stakingIdFactory: StakingIdFactory, - ): BaseCurrenciesStatusesOperations { - return CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - ) - } - @Provides @Singleton fun provideBaseCurrencyStatusOperations( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index c1d7cd8cc8..042f4d257f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { @OptIn(ExperimentalCoroutinesApi::class) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 1d02262cc7..0fc94390be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -20,7 +20,7 @@ import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { private val walletBalanceCache = ConcurrentHashMap() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index e39261d0a7..d8d52708eb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,6 +29,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -56,6 +58,8 @@ abstract class BaseCurrencyStatusOperations( protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() + abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> protected abstract suspend fun fetchComponents( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index fcce30d9fd..c82e831875 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -60,19 +60,18 @@ class CachedCurrenciesStatusesOperations( multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, -) : BaseCurrenciesStatusesOperations, - BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - tokensFeatureToggles = tokensFeatureToggles, - ) { +) : BaseCurrencyStatusOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, + tokensFeatureToggles = tokensFeatureToggles, +) { override fun getCurrenciesStatuses( userWalletId: UserWalletId, @@ -83,6 +82,7 @@ class CachedCurrenciesStatusesOperations( ) } + @Suppress("LongMethod") @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, @@ -163,10 +163,23 @@ class CachedCurrenciesStatusesOperations( .invokeOnCompletion { setFetchFinished(userWalletId) } } + val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) + combine( flow = getQuotes(currenciesIds), - flow2 = getNetworkStatusesUpdates(userWalletId, networks), - flow3 = getYieldsBalancesUpdates(userWalletId, currencies), + flow2 = networksStatusesUpdates, + flow3 = networksStatusesUpdates.flatMapLatest { + val currenciesAddresses = it.getOrElse(default = { emptySet() }) + .mapNotNull { + val currency = currencies.firstOrNull { currency -> currency.network == it.network } + ?: return@mapNotNull null + + currency.id to extractAddress(it) + } + .toMap() + + getYieldsBalancesUpdates(userWalletId, currenciesAddresses) + }, flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -379,24 +392,24 @@ class CachedCurrenciesStatusesOperations( // temporary code because token list is built using networks list private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, - cryptoCurrencies: List, + cryptoCurrencies: Map, ): EitherFlow> { return channelFlow { val state = MutableStateFlow(emptyList()) val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) + stakingIdFactory.create(currencyId = it.key, defaultAddress = it.value) .getOrNull() } - stakingIds.onEach { + stakingIds.onEach { stakingId -> launch { singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = it), + params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.stakingId == it } + loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } } } .launchIn(scope = this) From f0de02715173b2955eccde3a7d4166c3710c379e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 12:45:49 +0200 Subject: [PATCH 133/165] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 6 + .../common/ui/notifications/NotificationUM.kt | 2 + .../configs/feature_toggles_config.json | 4 + .../components/currency/icon/CurrencyIcon.kt | 10 +- .../currency/icon/CurrencyIconStateBuilder.kt | 103 ++++++ .../src/main/res/drawable/ic_copy_new_24.xml | 12 + core/ui/src/main/res/drawable/ic_ens_36.xml | 38 ++ .../main/res/drawable/ic_qrcode_new_24.xml | 9 + .../domain/models/TokenReceiveConfig.kt | 37 ++ .../domain/nft/GetNFTCurrencyUseCase.kt | 10 + .../tokenreceive/TokenReceiveFeatureToggle.kt | 6 + features/token-recieve/impl/build.gradle.kts | 2 + .../DefaultTokenReceiveFeatureToggle.kt | 11 + .../tokenreceive/entity/ReceiveAddress.kt | 16 + .../ui/ContainerWithSnackbarHost.kt | 23 ++ .../ui/TokenReceiveAssetsContent.kt | 346 ++++++++++++++++++ .../tokenreceive/ui/TokenReceiveContent.kt | 28 ++ .../ui/TokenReceiveQrCodeContent.kt | 195 ++++++++++ .../ui/TokenReceiveWarningContent.kt | 142 +++++++ .../tokenreceive/ui/state/QrCodeUM.kt | 11 + .../tokenreceive/ui/state/ReceiveAssetsUM.kt | 16 + .../tokenreceive/ui/state/TokenReceiveUM.kt | 15 + .../tokenreceive/ui/state/WarningUM.kt | 10 + 23 files changed, 1050 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt create mode 100644 core/ui/src/main/res/drawable/ic_copy_new_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_ens_36.xml create mode 100644 core/ui/src/main/res/drawable/ic_qrcode_new_24.xml create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt create mode 100644 features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index dcedc27851..4d82335720 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -154,4 +154,10 @@ internal object NFTDomainModule { ): ObserveAndClearNFTCacheIfNeedUseCase { return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) } + + @Provides + @Singleton + fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase { + return GetNFTCurrencyUseCase(nftRepository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index ff58fd1c80..1d4ff127d6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -273,6 +273,7 @@ sealed class NotificationUM(val config: NotificationConfig) { iconResId: Int = R.drawable.ic_alert_circle_24, buttonsState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, + iconTint: NotificationConfig.IconTint = NotificationConfig.IconTint.Unspecified, ) : NotificationUM( config = NotificationConfig( title = title, @@ -280,6 +281,7 @@ sealed class NotificationUM(val config: NotificationConfig) { iconResId = iconResId, buttonsState = buttonsState, onCloseClick = onCloseClick, + iconTint = iconTint, ), ) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6ecb2f5ac6..1158354430 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -50,5 +50,9 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NEW_TOKEN_RECEIVE_ENABLED", + "version": "undefined" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 4727ba9759..ff97530009 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme @@ -28,11 +29,16 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter * @param shouldDisplayNetwork specifies whether to display network badge */ @Composable -fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { +fun CurrencyIcon( + state: CurrencyIconState, + modifier: Modifier = Modifier, + shouldDisplayNetwork: Boolean = true, + iconSize: Dp = 36.dp, +) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) - .size(TangemTheme.dimens.size36) + .size(iconSize) when (state) { is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt new file mode 100644 index 0000000000..1aaed64244 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt @@ -0,0 +1,103 @@ +package com.tangem.core.ui.components.currency.icon + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.R +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.models.currency.CryptoCurrency + +object CurrencyIconStateBuilder { + + fun build( + cryptoCurrency: CryptoCurrency, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState = when (cryptoCurrency) { + is CryptoCurrency.Coin -> fromCoin(cryptoCurrency, isGrayscale, showCustomBadge) + is CryptoCurrency.Token -> fromToken(cryptoCurrency, isGrayscale, showCustomBadge) + } + + private fun createCoinIcon( + url: String? = null, + @DrawableRes fallbackResId: Int = R.drawable.ic_empty_64, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = false, + ): CurrencyIconState.CoinIcon = CurrencyIconState.CoinIcon( + url = url, + fallbackResId = fallbackResId, + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + + private fun createTokenIcon( + url: String? = null, + @DrawableRes topBadgeIconResId: Int? = null, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = false, + fallbackTint: Color = Color.Black, + fallbackBackground: Color = Color.White, + ): CurrencyIconState.TokenIcon = CurrencyIconState.TokenIcon( + url = url, + topBadgeIconResId = topBadgeIconResId, + isGrayscale = isGrayscale, + fallbackTint = fallbackTint, + fallbackBackground = fallbackBackground, + showCustomBadge = showCustomBadge, + ) + + private fun createCustomTokenIcon( + tint: Color, + background: Color, + @DrawableRes topBadgeIconResId: Int, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState.CustomTokenIcon = CurrencyIconState.CustomTokenIcon( + tint = tint, + background = background, + topBadgeIconResId = topBadgeIconResId, + isGrayscale = isGrayscale, + showCustomBadge = showCustomBadge, + ) + + private fun fromCoin( + coin: CryptoCurrency.Coin, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState.CoinIcon = createCoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = isGrayscale || coin.network.isTestnet, + showCustomBadge = coin.isCustom && showCustomBadge, + ) + + private fun fromToken( + token: CryptoCurrency.Token, + isGrayscale: Boolean = false, + showCustomBadge: Boolean = true, + ): CurrencyIconState { + val grayScale = isGrayscale || token.network.isTestnet + val background = token.tryGetBackgroundForTokenIcon(grayScale) + val tint = getTintForTokenIcon(background) + + return if (token.isCustom && token.iconUrl == null) { + createCustomTokenIcon( + tint = tint, + background = background, + topBadgeIconResId = token.networkIconResId, + isGrayscale = grayScale, + showCustomBadge = showCustomBadge, + ) + } else { + createTokenIcon( + url = token.iconUrl, + topBadgeIconResId = token.networkIconResId, + isGrayscale = grayScale, + fallbackTint = tint, + fallbackBackground = background, + showCustomBadge = token.isCustom && showCustomBadge, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_copy_new_24.xml b/core/ui/src/main/res/drawable/ic_copy_new_24.xml new file mode 100644 index 0000000000..a073a2b686 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_copy_new_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_ens_36.xml b/core/ui/src/main/res/drawable/ic_ens_36.xml new file mode 100644 index 0000000000..29b19ea2c7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_ens_36.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml new file mode 100644 index 0000000000..173a8c1979 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt new file mode 100644 index 0000000000..728496ef05 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +data class TokenReceiveConfig( + val shouldShowWarning: Boolean, + val cryptoCurrency: CryptoCurrency, + val userWalletId: UserWalletId, + val showMemoDisclaimer: Boolean, + val receiveAddress: List, + val tokenReceiveNotification: List = emptyList(), + val asset: Asset = Asset.Currency, +) + +@Serializable +data class ReceiveAddressModel( + val nameService: NameService, + val value: String, + val displayName: String, +) { + enum class NameService { + Default, Ens + } +} + +@Serializable +data class TokenReceiveNotification( + val title: Int, + val subtitle: Int, +) + +enum class Asset { + Currency, NFT +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt new file mode 100644 index 0000000000..85f77d810a --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.nft + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.nft.repository.NFTRepository + +class GetNFTCurrencyUseCase(private val nftRepository: NFTRepository) { + + operator fun invoke(network: Network): CryptoCurrency = nftRepository.getNFTCurrency(network) +} \ No newline at end of file diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..62175072dd --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt @@ -0,0 +1,6 @@ +package com.tangem.features.tokenreceive + +interface TokenReceiveFeatureToggle { + + val isNewTokenReceiveEnabled: Boolean +} \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index d46cd33aad..dfd0d93ada 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -44,11 +44,13 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.decompose) implementation(projects.core.res) + implementation(projects.core.configToggles) /** Domain modules */ implementation(projects.domain.models) implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) + implementation(projects.domain.tokens) /** Feature Apis */ implementation(projects.features.tokenRecieve.api) diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..209317d97b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt @@ -0,0 +1,11 @@ +package com.tangem.features.tokenreceive + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTokenReceiveFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) : TokenReceiveFeatureToggle { + + override val isNewTokenReceiveEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("NEW_TOKEN_RECEIVE_ENABLED") +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt new file mode 100644 index 0000000000..960dda1224 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt @@ -0,0 +1,16 @@ +package com.tangem.features.tokenreceive.entity + +import com.tangem.core.ui.extensions.TextReference + +internal data class ReceiveAddress( + val value: String, + val type: Type, +) { + sealed interface Type { + data object Ens : Type + + data class Default( + val displayName: TextReference, + ) : Type + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt new file mode 100644 index 0000000000..94415b927d --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt @@ -0,0 +1,23 @@ +package com.tangem.features.tokenreceive.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost + +@Composable +internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) { + Box { + content() + CopiedTextSnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 80.dp), + ) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt new file mode 100644 index 0000000000..10f41e7e05 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt @@ -0,0 +1,346 @@ +package com.tangem.features.tokenreceive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.res.getStringSafe +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.buttons.small.TangemIconButton +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.launch + +@Composable +internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) { + val snackbarHostState = remember(::SnackbarHostState) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 8.dp, + ), + ) { + SpacerH8() + + Info( + showMemoDisclaimer = assetsUM.showMemoDisclaimer, + notificationConfigs = assetsUM.notificationConfigs, + ) + + SpacerH12() + + AddressBlock( + onCopyClick = assetsUM.onCopyClick, + onOpenQrCodeClick = assetsUM.onOpenQrCodeClick, + addresses = assetsUM.addresses, + snackbarHostState = snackbarHostState, + fullName = assetsUM.fullName, + ) + + if (assetsUM.isEnsResultLoading) { + LoadingBlock() + } + } + } +} + +@Composable +private fun Info( + notificationConfigs: ImmutableList, + showMemoDisclaimer: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = 16.dp), + ) { + if (showMemoDisclaimer) { + Text( + modifier = Modifier + .padding(horizontal = 18.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } + + notificationConfigs.fastForEach { + key(it.hashCode()) { + Notification(config = it.config) + } + } + } +} + +@Composable +private fun AddressBlock( + onOpenQrCodeClick: (id: Int) -> Unit, + onCopyClick: (id: Int) -> Unit, + addresses: ImmutableMap, + snackbarHostState: SnackbarHostState, + fullName: String, +) { + val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = context.resources + + addresses.entries.toList().fastForEach { entry -> + when (entry.value.type) { + is ReceiveAddress.Type.Default -> { + key(entry.key) { + AddressItem( + onCopyClick = { + onCopyClick(entry.key) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe( + R.string.wallet_notification_address_copied, + ), + ) + } + }, + onOpenQrCodeClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onOpenQrCodeClick(entry.key) + }, + fullName = fullName, + address = entry.value.value, + ) + SpacerH8() + } + } + ReceiveAddress.Type.Ens -> { + key(entry.key) { + EnsItem( + onCopyClick = { + onCopyClick(entry.key) + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe( + R.string.wallet_notification_address_copied, + ), + ) + } + }, + address = entry.value.value, + ) + SpacerH8() + } + } + } + } +} + +@Composable +private fun AddressItem( + onOpenQrCodeClick: () -> Unit, + fullName: String, + onCopyClick: () -> Unit, + address: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + onClick = onOpenQrCodeClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + IdentIcon( + address = address, + modifier = Modifier + .size(size = 36.dp) + .clip(shape = RoundedCornerShape(18.dp)), + ) + + SpacerW12() + + Column(modifier = Modifier.weight(1f)) { + EllipsisText( + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName), + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + + EllipsisText( + text = address, + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } + + SpacerW12() + + TangemIconButton( + modifier = Modifier.size(TangemTheme.dimens.size28), + iconRes = R.drawable.ic_qrcode_new_24, + onClick = onOpenQrCodeClick, + ) + + SpacerW8() + + TangemIconButton( + modifier = Modifier.size(TangemTheme.dimens.size28), + iconRes = R.drawable.ic_copy_new_24, + onClick = onCopyClick, + ) + } + } +} + +@Composable +private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 14.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier.size(36.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_ens_36), + contentDescription = null, + ) + + SpacerW12() + + EllipsisText( + modifier = Modifier.weight(1f), + text = address, + ellipsis = TextEllipsis.Middle, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + ) + + TangemIconButton( + modifier = Modifier.size(28.dp), + iconRes = R.drawable.ic_copy_new_24, + onClick = onCopyClick, + ) + } + } +} + +@Composable +private fun LoadingBlock(modifier: Modifier = Modifier) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action), + ) { + Box( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 64.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(TangemTheme.dimens.spacing8), + ) + } + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveAssetsContent( + @PreviewParameter(TokenReceiveAssetsContentProvider::class) params: ReceiveAssetsUM, +) { + TangemThemePreview { + TokenReceiveAssetsContent(assetsUM = params) + } +} + +private class TokenReceiveAssetsContentProvider : PreviewParameterProvider { + val address = ReceiveAddress( + value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", + type = ReceiveAddress.Type.Default( + displayName = stringReference("Etherium address"), + ), + ) + private val config = ReceiveAssetsUM( + notificationConfigs = + persistentListOf( + NotificationUM.Warning( + title = stringReference("Send only XLM on the Ethereum network"), + subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), + ), + ), + addresses = persistentMapOf( + 0 to address, + 1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"), + ), + showMemoDisclaimer = false, + onCopyClick = {}, + onOpenQrCodeClick = {}, + isEnsResultLoading = true, + fullName = "Etherium", + ) + + override val values: Sequence + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt new file mode 100644 index 0000000000..5571b27657 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt @@ -0,0 +1,28 @@ +package com.tangem.features.tokenreceive.ui + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes + +@Composable +internal fun TokenReceiveContent( + stackState: ChildStack, + modifier: Modifier = Modifier, +) { + Children( + stack = stackState, + animation = stackAnimation(fade()), + modifier = modifier + .fillMaxSize() + .animateContentSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt new file mode 100644 index 0000000000..2eb6310cbd --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt @@ -0,0 +1,195 @@ +package com.tangem.features.tokenreceive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.res.getStringSafe +import com.tangem.core.ui.components.* +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tokenreceive.impl.R +import com.tangem.features.tokenreceive.ui.state.QrCodeUM +import kotlinx.coroutines.launch + +@Composable +internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) { + val snackbarHostState = remember(::SnackbarHostState) + val qrCodePainter = rememberQrPainter(content = qrCodeUM.addressValue) + + ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding(bottom = 16.dp) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH8() + + QrCodePage( + addressFullName = qrCodeUM.addressName, + addressValue = qrCodeUM.addressValue, + network = qrCodeUM.network, + qrCodePainter = qrCodePainter, + ) + SpacerH24() + + Buttons( + onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) }, + onCopyClick = { qrCodeUM.onCopyClick(qrCodeUM.addressValue) }, + snackbarHostState = snackbarHostState, + ) + } + } +} + +@Composable +private fun QrCodePage(addressFullName: TextReference, addressValue: String, network: String, qrCodePainter: Painter) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) { + Text( + text = stringResourceSafe( + R.string.receive_bottom_sheet_warning_message_compact, + addressFullName.resolveReference(), + network, + ), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + + SpacerH(20.dp) + + Image( + painter = qrCodePainter, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.size(248.dp), + ) + + SpacerH24() + } + Text( + text = stringResourceSafe(R.string.wc_common_address), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle2, + ) + + SpacerH2() + + Text( + text = addressValue, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle1, + ) + } +} + +@Composable +private fun Buttons( + snackbarHostState: SnackbarHostState, + onShareClick: () -> Unit, + onCopyClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val coroutineScope = rememberCoroutineScope() + val context = LocalContext.current + val resources = context.resources + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResourceSafe(id = R.string.common_copy), + iconResId = R.drawable.ic_copy_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onCopyClick() + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = resources.getStringSafe(R.string.wallet_notification_address_copied), + ) + } + }, + ) + + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResourceSafe(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onShareClick() + }, + ) + } +} + +@Composable +private fun rememberQrPainter(content: String, size: Dp = 248.dp, padding: Dp = 0.dp): BitmapPainter { + val density = LocalDensity.current + return remember(content) { + BitmapPainter( + content.toQrCode( + sizePx = with(density) { size.roundToPx() }, + paddingPx = with(density) { padding.roundToPx() }, + ).asImageBitmap(), + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveQrCodeContent( + @PreviewParameter(TokenReceiveQrCodeContentPreviewProvider::class) qrCodeUM: QrCodeUM, +) { + TangemThemePreview { + TokenReceiveQrCodeContent(qrCodeUM = qrCodeUM) + } +} + +private class TokenReceiveQrCodeContentPreviewProvider : PreviewParameterProvider { + private val config = QrCodeUM( + network = "Ethereum", + addressName = stringReference("Etherium"), + addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", + onCopyClick = {}, + onShareClick = {}, + ) + + override val values: Sequence + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt new file mode 100644 index 0000000000..2aa8aeff9c --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt @@ -0,0 +1,142 @@ +package com.tangem.features.tokenreceive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.stringResourceSafe +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.tokenreceive.impl.R +import com.tangem.features.tokenreceive.ui.state.WarningUM + +@Composable +internal fun TokenReceiveWarningContent(warningUM: WarningUM) { + val hapticFeedback = LocalHapticFeedback.current + + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CurrencyIcon( + modifier = Modifier.size(size = 56.dp), + state = warningUM.iconState, + shouldDisplayNetwork = false, + iconSize = 56.dp, + ) + + SpacerH24() + + WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network) + + SpacerH12() + + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + + SpacerH(48.dp) + + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.common_got_it), + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + warningUM.onWarningAcknowledged() + }, + ) + } +} + +@Composable +fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = networkIcon), + tint = Color.Unspecified, + contentDescription = null, + ) + + Text( + textAlign = TextAlign.Center, + text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TokenReceiveWarningContent( + @PreviewParameter(TokenReceiveWarningContentProvider::class) warningUM: WarningUM, +) { + TangemThemePreview { + TokenReceiveWarningContent(warningUM = warningUM) + } +} + +private class TokenReceiveWarningContentProvider : PreviewParameterProvider { + val iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = null, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + showCustomBadge = false, + ) + + override val values: Sequence + get() = sequenceOf( + WarningUM( + iconState = iconState, + onWarningAcknowledged = {}, + network = "Etherium", + networkIcon = R.drawable.ic_eth_16, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt new file mode 100644 index 0000000000..e3c4f5adf0 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.core.ui.extensions.TextReference + +internal data class QrCodeUM( + val onCopyClick: (String) -> Unit, + val onShareClick: (String) -> Unit, + val addressName: TextReference, + val addressValue: String, + val network: String, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt new file mode 100644 index 0000000000..3953389152 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal data class ReceiveAssetsUM( + val showMemoDisclaimer: Boolean, + val addresses: ImmutableMap, + val onOpenQrCodeClick: (id: Int) -> Unit, + val onCopyClick: (id: Int) -> Unit, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, + val fullName: String, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt new file mode 100644 index 0000000000..27eecde6a3 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal data class TokenReceiveUM( + val network: String, + val iconState: CurrencyIconState, + val addresses: ImmutableMap, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt new file mode 100644 index 0000000000..c31262fd58 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tokenreceive.ui.state + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState + +internal data class WarningUM( + val network: String, + val networkIcon: Int, + val iconState: CurrencyIconState, + val onWarningAcknowledged: () -> Unit, +) \ No newline at end of file From 1563b48e8a76c4fb20700a16694fd217268505f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 14:11:12 +0300 Subject: [PATCH 134/165] Updated on 2026-08-14 --- .../di/UserWalletsListManagerModule.kt | 3 + .../DefaultUserWalletsListRepository.kt | 39 ++++- .../local/preferences/PreferencesKeys.kt | 12 ++ .../data/wallets/di/WalletsDataModule.kt | 8 + ...ltHotWalletAccessCodeAttemptsRepository.kt | 138 ++++++++++++++++++ .../data/wallets/hot/HotWalletAccessor.kt | 35 ++++- .../HotWalletAccessCodeAttemptsRepository.kt | 71 +++++++++ .../wallets/hot/HotWalletPasswordRequester.kt | 34 ++++- .../DefaultHotAccessCodeRequestComponent.kt | 6 +- .../HotAccessCodeRequestModel.kt | 110 +++++++++++++- .../entity/HotAccessCodeRequestUM.kt | 2 + .../proxy/HotWalletPasswordRequesterProxy.kt | 17 +-- .../HotAccessCodeRequestFullScreenContent.kt | 36 ++++- .../welcome/impl/model/WelcomeModel.kt | 8 +- 14 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index d8173d8bc7..c521709443 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -14,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences @@ -120,6 +121,7 @@ internal object UserWalletsListManagerModule { dispatchers: CoroutineDispatcherProvider, passwordRequester: HotWalletPasswordRequester, appPreferencesStore: AppPreferencesStore, + hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -165,6 +167,7 @@ internal object UserWalletsListManagerModule { tangemSdkManagerProvider = Provider { tangemSdkManager }, appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now + hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d900b3f687..93810c48da 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -25,6 +25,8 @@ import com.tangem.domain.core.wallets.error.SetLockError import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.utils.encryptionKey @@ -37,7 +39,7 @@ import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( private val publicInformationRepository: UserWalletsPublicInformationRepository, private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, @@ -47,6 +49,7 @@ internal class DefaultUserWalletsListRepository( private val tangemSdkManagerProvider: Provider, private val savePersistentInformation: ProviderSuspend, private val appPreferencesStore: AppPreferencesStore, + private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -225,6 +228,7 @@ internal class DefaultUserWalletsListRepository( } val encryptionKey = requestPasswordRecursive( + hotWalletId = userWallet.hotWalletId, block = { password -> runCatching { userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) @@ -241,6 +245,8 @@ internal class DefaultUserWalletsListRepository( return@either } + removePasswordAttempts(userWallet) + sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } .doOnFailure { error -> @@ -282,15 +288,26 @@ internal class DefaultUserWalletsListRepository( val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() val allKeys = (biometricKeys + unsecuredKeys).distinct() - val unlockedWallets = allKeys.map { it.walletId } + val unlockedWalletsIds = allKeys.map { it.walletId } + + val unlockedWallets = unlockedWalletsIds.mapNotNull { id -> + userWalletsSync().firstOrNull { it.walletId == id } + } + + // Remove all password attempts for unlocked hot wallets + unlockedWallets.forEach { + removePasswordAttempts(it) + } // if we cant unlock all wallets - if (userWalletIds.all { it in unlockedWallets }.not()) { + if (userWalletIds.all { it in unlockedWalletsIds }.not()) { raise(UnlockWalletError.UnableToUnlock) } sensitiveInformationRepository.getAll(allKeys) - .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnSuccess { sensitiveInfo -> + userWallets.update { it?.updateWith(sensitiveInfo) } + } .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } @@ -319,12 +336,16 @@ internal class DefaultUserWalletsListRepository( } private suspend fun requestPasswordRecursive( + hotWalletId: HotWalletId, block: suspend (CharArray) -> UserWalletEncryptionKey?, biometryFallback: suspend () -> Either, ): Either { - val result = passwordRequester.requestPassword( + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts hasBiometry = hasBiometry(), ) + val result = passwordRequester.requestPassword(attemptRequest) return when (result) { HotWalletPasswordRequester.Result.Dismiss -> { @@ -335,7 +356,7 @@ internal class DefaultUserWalletsListRepository( val decrypted = block(result.password.value) if (decrypted == null) { passwordRequester.wrongPassword() - requestPasswordRecursive(block, biometryFallback) + requestPasswordRecursive(hotWalletId, block, biometryFallback) } else { passwordRequester.successfulAuthentication() passwordRequester.dismiss() @@ -353,6 +374,12 @@ internal class DefaultUserWalletsListRepository( } } + private suspend fun removePasswordAttempts(userWallet: UserWallet) { + if (userWallet is UserWallet.Hot) { + hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId) + } + } + private suspend fun hasBiometry(): Boolean { val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 6c0e164b4e..06a5f51102 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -169,6 +169,18 @@ object PreferencesKeys { fun getShouldShowInitialPermissionScreen(permission: String) = booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") // endregion + + // region Hot Wallet unlock attempts + + fun getHotWalletUnlockAttemptsKey(attemptId: String) = + intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId") + + fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId") + + fun getHotWalletUnlockDeadlineKey(attemptId: String) = + longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId") + + // endregion } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 5564622241..bcaff10b6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.derivations.DefaultDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore @@ -13,6 +14,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -68,4 +70,10 @@ internal interface WalletsDataBindsModule { @Binds @Singleton fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository + + @Binds + @Singleton + fun bindHotWalletAccessCodeAttemptsRepository( + impl: DefaultHotWalletAccessCodeAttemptsRepository, + ): HotWalletAccessCodeAttemptsRepository } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..8a77f2f70a --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,138 @@ +package com.tangem.data.wallets.hot + +import android.content.Context +import android.os.SystemClock +import android.provider.Settings +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS +import com.tangem.hot.sdk.model.HotWalletId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@Suppress("MagicNumber") +class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val appPreferencesStore: AppPreferencesStore, +) : HotWalletAccessCodeAttemptsRepository { + + override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey()) + + appPreferencesStore.editData { preferences -> + val currentAttempts = preferences[attemptsKey] ?: 0 + val newAttempts = currentAttempts + 1 + + preferences[attemptsKey] = newAttempts + val currentBootCount = currentBootCount() + preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount + + if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) { + val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000 + preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline + } + } + } + + override suspend fun resetAttempts(hotWalletId: HotWalletId) { + val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = true, + ) + val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = false, + ) + + appPreferencesStore.editData { + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow { + val flow = appPreferencesStore.data.map { + AttemptsPersistentData( + attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0, + bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0, + deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L, + ) + }.distinctUntilChanged() + + return flow.transformLatest { + while (true) { + emit(toState(id, it.attempts, it.deadline, it.bootCount)) + val remaining = remainingSeconds(it.deadline, it.bootCount) + if (remaining <= 0) break + delay(timeMillis = 1000) + } + }.distinctUntilChanged() + } + + override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts { + val prefs = appPreferencesStore.data.first() + val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0 + val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0 + val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L + return toState(id, count, deadline, boot) + } + + private fun remainingSeconds(deadline: Long, bootStored: Int): Int { + val now = SystemClock.elapsedRealtime() + val bootNow = currentBootCount() + if (bootNow != bootStored) { + // If the boot happened after the last attempt, we consider timer to start from the beginning + return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt()) + } + return maxOf(0, ((deadline - now) / 1000).toInt()) + } + + private fun toState( + id: HotWalletAccessCodeAttemptsRepository.AttemptId, + count: Int, + deadlineElapsed: Long, + bootStored: Int, + ): Attempts { + val fast = MAX_FAST_FORWARD_ATTEMPTS + val attention = ATTEMPTS_BEFORE_DELETION + val deletion = MAX_ATTEMPTS_BEFORE_DELETION + + return when { + count < fast -> Attempts.FastForward(count) + id.auth && count >= deletion -> Attempts.Deletion + id.auth && count >= attention -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.BeforeDeletion(count, remaining, deletion - count) + } + else -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.WithDelay(count, remaining) + } + } + } + + private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { + return "${hotWalletId.value}_$auth" + } + + private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) + + private data class AttemptsPersistentData( + val attempts: Int, + val bootCount: Int, + val deadline: Long, + ) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index b53cd193d1..b5ca40b2de 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -33,10 +33,16 @@ class HotWalletAccessor @Inject constructor( val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth - HotWalletId.AuthType.Password -> requestPassword(false) + HotWalletId.AuthType.Password -> requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) HotWalletId.AuthType.Biometry -> { if (isAccessCodeRequired) { - requestPassword(false) + requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) } else { HotAuth.Biometry } @@ -56,6 +62,7 @@ class HotWalletAccessor @Inject constructor( block: suspend (auth: HotAuth) -> T, ): T { return runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = auth, auth = auth, block = { blockAuth -> @@ -97,6 +104,7 @@ class HotWalletAccessor @Inject constructor( } private suspend fun runCatchingWrongPassInternal( + hotWalletId: HotWalletId, originalAuth: HotAuth, auth: HotAuth, block: suspend (auth: HotAuth) -> T, @@ -105,9 +113,13 @@ class HotWalletAccessor @Inject constructor( }.getOrElse { exception -> if (auth is HotAuth.Biometry && exception.isBiometryError()) { // fallback to password if biometry fails - val passAuth = requestPassword(true) + val passAuth = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = true, + ) return@getOrElse runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passAuth, block = block, @@ -121,17 +133,28 @@ class HotWalletAccessor @Inject constructor( // If the exception is a wrong password, we need to request the password again hotWalletPasswordRequester.wrongPassword() - val passResult = requestPassword(originalAuth is HotAuth.Biometry) + val passResult = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = originalAuth is HotAuth.Biometry, + ) runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passResult, block = block, ) } - private suspend fun requestPassword(hasBiometry: Boolean): HotAuth { - return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled() + private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth { + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = false, + hasBiometry = hasBiometry, + ) + + return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth() + ?: throw TangemSdkError.UserCancelled() } private fun Throwable.isBiometryError(): Boolean { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..48c37f6440 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.HotWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for managing access code attempts for hot wallets. + * It tracks the number of attempts made to access a hot wallet and applies cooldowns or deletion + * based on the number of attempts. + */ +interface HotWalletAccessCodeAttemptsRepository { + + /** + * Increments the number of attempts for the given [AttemptId]. + * If the number of attempts exceeds [MAX_FAST_FORWARD_ATTEMPTS], a cooldown period is initiated. + */ + suspend fun incrementAttempts(id: AttemptId) + + /** + * Resets the attempts for the given [HotWalletId]. + * This is typically called when the user successfully authenticates or when the wallet is deleted. + */ + suspend fun resetAttempts(hotWalletId: HotWalletId) + + /** + * Retrieves the current attempts for the given [AttemptId]. + * The result is a flow that emits the current state of attempts. + */ + fun getAttempts(id: AttemptId): Flow + + /** + * Synchronously retrieves the current attempts for the given [AttemptId]. + * This is useful when you need to get the attempts without using a flow. + */ + suspend fun getAttemptsSync(id: AttemptId): Attempts + + data class AttemptId( + val hotWalletId: HotWalletId, + val auth: Boolean, + ) + + sealed interface Attempts { + val count: Int + + data class FastForward( + override val count: Int, + ) : Attempts + + data class WithDelay( + override val count: Int, + val remainingSeconds: Int, + ) : Attempts + + data class BeforeDeletion( + override val count: Int, + val remainingSeconds: Int, + val remainingAttemptsCountBeforeDeletion: Int, + ) : Attempts + + data object Deletion : Attempts { + override val count: Int = MAX_ATTEMPTS_BEFORE_DELETION + } + } + + companion object { + const val COOLDOWN_SECONDS = 60 + const val MAX_FAST_FORWARD_ATTEMPTS = 5 + const val ATTEMPTS_BEFORE_DELETION = 20 + const val MAX_ATTEMPTS_BEFORE_DELETION = 30 + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index 7dcc5fa579..c3f3df8d95 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -1,17 +1,49 @@ package com.tangem.domain.wallets.hot import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId +/** + * Interface for requesting the password for a hot wallet. + * It provides methods to handle password requests, authentication states, and user interactions. + */ interface HotWalletPasswordRequester { + /** + * Sets state to show wrong password state. + */ suspend fun wrongPassword() + /** + * Sets state to show successful authentication state. + */ suspend fun successfulAuthentication() - suspend fun requestPassword(hasBiometry: Boolean): Result + /** + * Requests the user to enter the password for the hot wallet. + * @param attemptRequest Contains information about the hot wallet and authentication mode. + * @return Result of the password request, which can be either a password entry, biometric use, or dismissal. + */ + suspend fun requestPassword(attemptRequest: AttemptRequest): Result + /** + * Dismisses the password request dialog. + */ suspend fun dismiss() + /** + * Represents a request to authenticate with a hot wallet. + * @param hotWalletId The ID of the hot wallet to authenticate with. + * @param authMode Indicates whether the request is for authentication mode. + * In auth mode user can be deleted after failed attempts. + * @param hasBiometry Indicates whether to show biometric authentication option. + */ + data class AttemptRequest( + val hotWalletId: HotWalletId, + val authMode: Boolean, + val hasBiometry: Boolean, + ) + sealed class Result { data object UseBiometry : Result() data object Dismiss : Result() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index 6777d34243..4dd7d01895 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -29,8 +29,10 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( model.successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { - model.show(hasBiometry) + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result { + model.show(attemptRequest) return model.waitResult() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index f210270b41..4a75ad8438 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -3,37 +3,63 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM +import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { private val result = MutableStateFlow(null) + private val currentRequest = MutableStateFlow(null) + private val attemptsRequestJobHolder = JobHolder() + + private val HotWalletPasswordRequester.AttemptRequest.attemptId + get() = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = authMode, + ) val uiState: StateFlow field = MutableStateFlow(getInitialState()) - fun dismiss() { - result.value = HotWalletPasswordRequester.Result.Dismiss - dismissState() - } + suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { + if (userWalletExists(attemptRequest.hotWalletId).not()) { + Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + result.value = HotWalletPasswordRequester.Result.Dismiss + return + } - fun show(hasBiometry: Boolean) { + currentRequest.value = attemptRequest result.value = null // Reset the result when showing the dialog + subscribeToAttempts(id = attemptRequest.attemptId) uiState.update { it.copy( isShown = true, accessCode = "", - useBiometricVisible = hasBiometry, + useBiometricVisible = attemptRequest.hasBiometry, onAccessCodeChange = ::onAccessCodeChange, ) } @@ -43,7 +69,15 @@ internal class HotAccessCodeRequestModel @Inject constructor( return result.filterNotNull().first().also { result.value = null } } + fun dismiss() { + result.value = HotWalletPasswordRequester.Result.Dismiss + attemptsRequestJobHolder.cancel() + dismissState() + } + suspend fun wrongAccessCode() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.incrementAttempts(currentRequest.attemptId) uiState.update { it.copy( accessCodeColor = PinTextColor.WrongCode, @@ -54,6 +88,8 @@ internal class HotAccessCodeRequestModel @Inject constructor( } suspend fun successfulAuthentication() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.resetAttempts(currentRequest.hotWalletId) uiState.update { it.copy( accessCodeColor = PinTextColor.Success, @@ -92,6 +128,68 @@ internal class HotAccessCodeRequestModel @Inject constructor( } } + private fun subscribeToAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + fun remainingSecondsToText(remainingSeconds: Int): TextReference? { + return if (remainingSeconds > 0) { + resourceReference( + R.string.access_code_check_warining_wait, + wrappedList(remainingSeconds), + ) + } else { + null + } + } + + suspend fun collectAttempts(attempts: Attempts) { + when (attempts) { + is Attempts.FastForward -> { + /** ignore */ + } + is Attempts.WithDelay -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + is Attempts.BeforeDeletion -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds) + ?: resourceReference( + R.string.access_code_check_warining_delete, + wrappedList(attempts.remainingAttemptsCountBeforeDeletion), + ), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + Attempts.Deletion -> deleteUserWallet() + } + } + + modelScope.launch { + hotAccessCodeAttemptsRepository.getAttempts(id) + .collectLatest { attempts -> collectAttempts(attempts) } + }.saveIn(attemptsRequestJobHolder) + } + + private suspend fun userWalletExists(id: HotWalletId): Boolean { + return userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot && it.hotWalletId == id } + } + + private suspend fun deleteUserWallet() { + val currentRequest = currentRequest.value ?: return + val userWallet = userWalletsListRepository.userWalletsSync() + .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + userWalletsListRepository.delete(listOf(userWallet.walletId)) + dismiss() + } + private fun dismissState() { uiState.update { it.copy(isShown = false) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 62f2b4d051..78c3c269f6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,11 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest.entity import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", val accessCodeColor: PinTextColor = PinTextColor.Primary, + val wrongAccessCodeText: TextReference? = null, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index b7e7218a48..1968bae87c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -13,20 +13,15 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR val componentRequester = MutableStateFlow(null) - override suspend fun wrongPassword() { - call { wrongPassword() } - } + override suspend fun wrongPassword() = call { wrongPassword() } - override suspend fun successfulAuthentication() { - call { successfulAuthentication() } - } + override suspend fun successfulAuthentication() = call { successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result = - call { requestPassword(hasBiometry) } + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) } - override suspend fun dismiss() { - call { dismiss() } - } + override suspend fun dismiss() = call { dismiss() } private suspend fun call(block: suspend HotWalletPasswordRequester.() -> T): T { return withTimeout(timeMillis = 1000) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index 9ae3230ec4..cc30961992 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -13,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton @@ -22,6 +24,8 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager @@ -89,6 +93,33 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) + + SpacerH(20.dp) + + AnimatedVisibility( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween()), + exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), + ), + visible = state.wrongAccessCodeText != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + val wrongAccessCodeText = + state.wrongAccessCodeText ?: return@AnimatedVisibility + + Text( + text = wrongAccessCodeText.resolveReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.warning, + ) + } } if (state.useBiometricVisible) { @@ -132,7 +163,10 @@ private fun Preview() { var isShown by remember { mutableStateOf(true) } HotAccessCodeRequestFullScreenContent( - state = HotAccessCodeRequestUM(isShown = isShown), + state = HotAccessCodeRequestUM( + isShown = isShown, + wrongAccessCodeText = stringReference("Wrong access code"), + ), modifier = Modifier, ) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 1901cc427a..8ef83bf9ec 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -76,7 +76,13 @@ internal class WelcomeModel @Inject constructor( launch { walletsFetcher.userWallets - .collectLatest { wallets.value = it } + .collectLatest { + if (it.isEmpty()) { + router.replaceAll(AppRoute.Home()) + } + + wallets.value = it + } } tryToUnlockRightAway() From c63d35bb8b48af04d9a09a16bc240414d9c59d97 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 16:13:28 +0500 Subject: [PATCH 135/165] Updated on 2026-08-14 --- .../hotwallet/accesscode/ui/AccessCode.kt | 2 +- .../model/AddExistingWalletImportModel.kt | 35 ++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 230f97bab4..3924d978c7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -74,7 +74,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = true, + isPasswordVisual = !state.isConfirmMode, value = state.accessCode, pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index d495361f44..60f3305500 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -12,8 +12,10 @@ import com.tangem.core.ui.components.bottomsheets.message.infoBlock import com.tangem.core.ui.components.bottomsheets.message.onClick import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.MnemonicRepository @@ -83,26 +85,41 @@ internal class AddExistingWalletImportModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { modelScope.launch { - uiState.update { - it.copy(importWalletProgress = true) - } + setImportProgress(true) runCatching { val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() - saveUserWalletUseCase(userWallet.copy(backedUp = true)) - params.callbacks.onWalletImported(userWallet.walletId) + saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) + .onLeft { + setImportProgress(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> { + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), + ) + } + } + } + .onRight { + setImportProgress(false) + params.callbacks.onWalletImported(userWallet.walletId) + } }.onFailure { Timber.e(it) - - uiState.update { - it.copy(importWalletProgress = false) - } + setImportProgress(false) } } } + private fun setImportProgress(progress: Boolean) { + uiState.update { + it.copy(importWalletProgress = progress) + } + } + private fun onPassphraseInfoClick() { uiMessageSender.send(passphraseInfoAlertBS) } From 11eb1bbbdca87365ee10435873c73878218d5f24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 14:31:47 +0300 Subject: [PATCH 136/165] Updated on 2026-08-14 --- .../com/tangem/tap/routing/utils/ChildFactory.kt | 5 +---- .../kotlin/com/tangem/common/routing/AppRoute.kt | 2 ++ .../ui/HotAccessCodeRequestFullScreenContent.kt | 5 ++++- .../com/tangem/features/welcome/WelcomeComponent.kt | 9 +-------- .../features/welcome/impl/DefaultWelcomeComponent.kt | 6 +++--- .../features/welcome/impl/model/WelcomeModel.kt | 2 -- .../features/welcome/impl/ui/AddWalletBottomSheet.kt | 11 ++++++----- .../features/welcome/impl/ui/WelcomeSelectWallet.kt | 12 ++++++++---- 8 files changed, 25 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index d4b9a8ee88..49bb638a53 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -149,10 +149,7 @@ internal class ChildFactory @Inject constructor( if (hotWalletFeatureToggles.isHotWalletEnabled) { createComponentChild( context = context, - params = NewWelcomeComponent.Params( - launchMode = route.launchMode, - intent = route.intent, - ), + params = Unit, componentFactory = newWelcomeComponentFactory, ) } else { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 719d1dd05d..f797561ced 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -30,8 +30,10 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Welcome( + @Deprecated("No longer used, will be removed in future releases") val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, // we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware + @Deprecated("No longer used, will be removed in future releases") val intent: SerializableIntent? = null, ) : AppRoute(path = "/welcome"), RouteBundleParams { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index cc30961992..2e3b61e43d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -129,7 +129,10 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM .fillMaxWidth() .navigationBarsPadding() .imePadding(), - text = "Use biometric", + text = stringResourceSafe( + id = R.string.welcome_unlock, + stringResourceSafe(R.string.common_biometrics), + ), onClick = state.useBiometricClick, ) } diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt index 6d9044f216..bc7092a29c 100644 --- a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -1,16 +1,9 @@ package com.tangem.features.welcome -import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { - data class Params( - val launchMode: InitScreenLaunchMode, - val intent: SerializableIntent?, - ) - - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt index 7bc6a1d982..8638078499 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt @@ -15,10 +15,10 @@ import dagger.assisted.AssistedInject internal class DefaultWelcomeComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: WelcomeComponent.Params, + @Assisted val params: Unit, ) : WelcomeComponent, AppComponentContext by context { - private val model: WelcomeModel = getOrCreateModel(params) + private val model: WelcomeModel = getOrCreateModel() @Composable override fun Content(modifier: Modifier) { @@ -32,6 +32,6 @@ internal class DefaultWelcomeComponent @AssistedInject constructor( @AssistedFactory interface Factory : WelcomeComponent.Factory { - override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + override fun create(context: AppComponentContext, params: Unit): DefaultWelcomeComponent } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 8ef83bf9ec..c189ac83b5 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -48,8 +48,6 @@ internal class WelcomeModel @Inject constructor( private val walletsRepository: WalletsRepository, ) : Model() { - // TODO add intent handling - // val params val uiState: StateFlow field = MutableStateFlow(WelcomeUM.Plain) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt index 4331461929..5d26549787 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt @@ -12,16 +12,17 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM @Composable fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - titleText = TextReference.Str("Add Wallet"), + titleText = resourceReference(R.string.auth_info_add_wallet_title), containerColor = TangemTheme.colors.background.tertiary, content = { Content(it) }, ) @@ -38,7 +39,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { ), ) { InputRowDefault( - text = TextReference.Str("Create New Wallet"), + text = resourceReference(R.string.home_button_create_new_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 0, @@ -49,7 +50,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, ) InputRowDefault( - text = TextReference.Str("Add Existing Wallet"), + text = resourceReference(R.string.home_button_add_existing_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 1, @@ -60,7 +61,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, ) InputRowDefault( - text = TextReference.Str("Buy Tangem Wallet"), + text = resourceReference(R.string.details_buy_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 2, diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index 305b00750c..9b3bba406d 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -22,6 +22,7 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.* import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.WelcomeUM @@ -78,7 +79,10 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal .padding(16.dp) .navigationBarsPadding() .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", + text = stringResourceSafe( + R.string.user_wallet_list_unlock_all_with, + stringResourceSafe(id = R.string.common_biometrics), + ), onClick = state.onUnlockWithBiometricClick, ) } @@ -122,7 +126,7 @@ private fun AnimatedContentScope.TopBar(state: WelcomeUM.SelectWallet, modifier: TextButton( modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), - text = "Add Wallet", + text = stringResourceSafe(R.string.auth_info_add_wallet_title), colors = TangemButtonsDefaults.defaultTextButtonColors.copy( contentColor = TangemTheme.colors.text.primary1, ), @@ -146,7 +150,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Welcome back!", + text = stringResourceSafe(R.string.auth_info_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -161,7 +165,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Select a wallet to log in", + text = stringResourceSafe(R.string.auth_info_subtitle), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, ) From be5786260aa9beb7581e82890cd8597e7be25229 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 15:25:19 +0300 Subject: [PATCH 137/165] Updated on 2026-08-14 --- app/build.gradle.kts | 2 +- .../tangem/common/rules/ApiEnvironmentRule.kt | 1 + .../screens/StakingDetailsPageObject.kt | 117 ++++++ .../screens/StakingSendDetailsPageObject.kt | 51 +++ .../tangem/screens/StakingSendPageObject.kt | 78 ++++ .../com/tangem/screens/SwapTokenPageObject.kt | 7 +- .../tangem/screens/TokenDetailsPageObject.kt | 60 ++- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 7 - .../com/tangem/tests/OrganizeTokensTest.kt | 5 + .../kotlin/com/tangem/tests/StakingTest.kt | 379 ++++++++++++++++++ .../common/ui/amountScreen/ui/AmountBlock.kt | 8 +- .../ui/amountScreen/ui/AmountButtons.kt | 12 +- .../common/ui/amountScreen/ui/AmountField.kt | 5 +- .../amountScreen/ui/AmountFieldContainer.kt | 8 +- .../NavigationButtonsBlock.kt | 5 +- core/datasource/build.gradle.kts | 2 +- .../datasource/api/common/config/StakeKit.kt | 35 +- .../datasource/di/utils/RetrofitApiBuilder.kt | 3 +- .../ui/components/fields/AmountTextField.kt | 6 +- .../ui/components/inputrow/InputRowDefault.kt | 9 +- .../ui/components/rows/RoundableCornersRow.kt | 7 +- .../tangem/core/ui/test/BaseBlockTestTags.kt | 7 + .../ui/test/StakingDetailsScreenTestTags.kt | 15 + .../test/StakingSendDetailsScreenTestTags.kt | 10 + .../core/ui/test/StakingSendScreenTestTags.kt | 16 + .../core/ui/test/SwapTokenScreenTestTags.kt | 1 - .../ui/test/TokenDetailsScreenTestTags.kt | 12 + .../ui/StakingInitialInfoContent.kt | 12 +- .../impl/presentation/ui/StakingScreen.kt | 5 +- .../impl/presentation/ui/StakingTosText.kt | 4 + .../presentation/ui/block/StakingFeeBlock.kt | 5 +- .../presentation/ui/block/ValidatorBlock.kt | 5 +- .../components/staking/StakingBalanceBlock.kt | 11 +- .../components/staking/TokenStakingBlock.kt | 11 +- 34 files changed, 873 insertions(+), 48 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b22a62d437..4de1a30e45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -349,7 +349,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index 0b0f1a32d0..a8d1a2c038 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule { ApiConfig.ID.TangemTech, ApiConfig.ID.Express, ApiConfig.ID.TangemPay, + ApiConfig.ID.StakeKit, ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt new file mode 100644 index 0000000000..7e118e51b1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt @@ -0,0 +1,117 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.* +import com.tangem.features.tokendetails.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.staking.impl.R as StakingImplR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) + } + + val stakingTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val bannerImage: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_IMAGE) + useUnmergedTree = true + } + + val bannerText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_TEXT) + useUnmergedTree = true + } + + val annualPercentageRate: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_annual_percentage_rate)) + useUnmergedTree = true + } + + val availableBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_available)) + useUnmergedTree = true + + } + + val unbondingPeriodBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_unbonding_period)) + useUnmergedTree = true + } + + val rewardClaimingBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_claiming)) + useUnmergedTree = true + } + + val rewardScheduleBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_schedule)) + useUnmergedTree = true + } + + val rewardsBlock: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK) + useUnmergedTree = true + } + + val rewardsBlockTitle: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TITLE) + useUnmergedTree = true + } + + val rewardsBlockText: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TEXT) + useUnmergedTree = true + } + + val yourStakesTitle: KNode = child { + hasText(getResourceString(StakingImplR.string.staking_your_stakes)) + useUnmergedTree = true + } + + val activeStakingBlock: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK) + useUnmergedTree = true + } + + val toSText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.TOS_TEXT) + useUnmergedTree = true + } + + val stakeMoreButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.staking_stake_more)) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingDetailsScreen(function: StakingDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt new file mode 100644 index 0000000000..79b38d7c9c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt @@ -0,0 +1,51 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val primaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val validatorBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK) + useUnmergedTree = true + } + + val networkFeeBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt new file mode 100644 index 0000000000..f71c9ad210 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt @@ -0,0 +1,78 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.send.v2.impl.R as SendR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER) + } + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val amountContainerTitle: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE) + useUnmergedTree = true + } + + val amountContainerText: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT) + useUnmergedTree = true + } + + val amountInputTextField: KNode = child { + hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val currencyButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON)) + useUnmergedTree = true + } + + val fiatButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON)) + useUnmergedTree = true + } + + val maxButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.MAX_BUTTON) + useUnmergedTree = true + } + + val previousButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON) + useUnmergedTree = true + } + + val nextButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(SendR.string.common_next)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 3ba4c711b5..dd53bc1dc5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -3,10 +3,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.R -import com.tangem.core.ui.test.BaseButtonTestTags -import com.tangem.core.ui.test.NotificationTestTags -import com.tangem.core.ui.test.SwapTokenScreenTestTags -import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.test.* import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -32,7 +29,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } val networkFeeBlock: KNode = child { - hasTestTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK) + hasTestTag(BaseBlockTestTags.BLOCK) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index e6bb7ddeb4..128a24ad03 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -4,15 +4,16 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.TokenDetailsScreenTestTags -import com.tangem.core.ui.utils.LazyListItemPositionSemantics import com.tangem.features.tokendetails.impl.R +import com.tangem.core.ui.utils.LazyListItemPositionSemantics import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode -import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -21,6 +22,61 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) } + val availableStakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK) + useUnmergedTree = true + } + + val stakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_BLOCK) + useUnmergedTree = true + } + + val availableStakingBlockTitle: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE) + useUnmergedTree = true + } + + val availableStakingBlockText: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT) + useUnmergedTree = true + } + + val availableStakingBlockCurrencyIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + + val stakingFiatAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) + useUnmergedTree = true + } + + val stakingDot: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT) + useUnmergedTree = true + } + + val stakingTokenAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) + useUnmergedTree = true + } + + val stakingChevronIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON) + useUnmergedTree = true + } + + val stakingTitle: KNode = child { + hasText(getResourceString(R.string.staking_native)) + } + val title: KNode = child { hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 62365124a9..f553c1cbe0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -371,13 +371,6 @@ class BuyTokenTest : BaseTestCase() { step("Open 'Select Provider' bottom sheet") { onBuyTokenDetailsScreen { providerTitle.performClick() } } - step("Assert unavailable provider name is displayed") { - onSelectProviderBottomSheet { - flakySafely(WAIT_UNTIL_TIMEOUT) { - unavailableProviderItem.assertIsDisplayed() - } - } - } step("Assert available provider name is displayed") { onSelectProviderBottomSheet { flakySafely(WAIT_UNTIL_TIMEOUT) { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 44cab70be3..76d8e34a7a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -31,6 +31,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -55,6 +56,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -105,6 +107,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -140,6 +143,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -192,6 +196,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt new file mode 100644 index 0000000000..083478f475 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -0,0 +1,379 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class StakingTest : BaseTestCase() { + + @AllureId("3558") + @DisplayName("Staking: validate staking block on 'Token details' screen") + @Test + fun validateStakingBlockTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Staking block' is displayed") { + onTokenDetailsScreen { stakingBlock.assertIsDisplayed() } + } + step("Assert 'Staking title' is displayed") { + onTokenDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Staking fiat amount' is displayed") { + onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() } + } + step("Assert 'Staking dot' is displayed") { + onTokenDetailsScreen { stakingDot.assertIsDisplayed() } + } + step("Assert 'Staking token amount' is displayed") { + onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() } + } + step("Assert 'Staking block chevron icon' is displayed") { + onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() } + } + } + } + + @AllureId("3550") + @DisplayName("Staking: validate staking more screens") + @Test + fun validateStakingMoreScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Click on 'Staking block'") { + onTokenDetailsScreen { stakingBlock.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' is displayed") { + onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' title is displayed") { + onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() } + } + step("Assert 'Rewards block' text is displayed") { + onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() } + } + step("Assert 'Active staking block' is displayed") { + onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() } + } + step("Assert 'Your stakes' title is displayed") { + onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake more' button is displayed") { + onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() } + } + step("Click 'Stake more' button") { + onStakingDetailsScreen { stakeMoreButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } + + @AllureId("3548") + @DisplayName("Staking: validate staking screens") + @Test + fun validateStakingScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Started" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Available staking block' is displayed") { + onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() } + } + step("Assert 'Available staking block' title is displayed") { + onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() } + } + step("Assert 'Available staking block' text is displayed") { + onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() } + } + step("Assert 'Available staking block' currency icon is displayed") { + onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() } + } + step("Click on 'Stake' button") { + onTokenDetailsScreen { stakeButton.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert banner image is displayed") { + onStakingDetailsScreen { bannerImage.assertIsDisplayed() } + } + step("Assert banner text is displayed") { + onStakingDetailsScreen { bannerText.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingDetailsScreen { stakeButton.assertIsDisplayed() } + } + step("Click 'Stake' button") { + onStakingDetailsScreen { stakeButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index e4f345ca11..47418a9338 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,6 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import java.math.BigDecimal @Composable @@ -63,7 +65,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis maxLines = 1, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing24), + .padding(top = TangemTheme.dimens.spacing24) + .testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT), ) Text( text = secondAmount, @@ -72,7 +75,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing8), + .padding(top = TangemTheme.dimens.spacing8) + .testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index f1d8e921a8..753a0fc99e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import kotlinx.collections.immutable.PersistentList private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" @@ -77,7 +79,8 @@ internal fun LazyListScope.buttons( .padding( vertical = TangemTheme.dimens.spacing10, horizontal = TangemTheme.dimens.spacing34, - ), + ) + .testTag(StakingSendScreenTestTags.MAX_BUTTON), ) } } @@ -90,7 +93,8 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing10, - ), + ) + .testTag(StakingSendScreenTestTags.CURRENCY_BUTTON), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { @@ -102,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment url = button.iconUrl, size = TangemTheme.dimens.size18, isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON), ) } else if (button.iconState != null) { CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON), ) } Text( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 35d49a5387..f491629420 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import com.tangem.common.ui.amountScreen.models.AmountFieldModel @@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @@ -116,7 +118,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri textAlign = TextAlign.Center, modifier = Modifier .align(TopCenter) - .padding(bottom = TangemTheme.dimens.spacing32), + .padding(bottom = TangemTheme.dimens.spacing32) + .testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT), ) AmountFieldError( isError = amountField.isError, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 9c31705709..ea0473e930 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.R @@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -52,7 +54,8 @@ internal fun LazyListScope.amountField( style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14), + .padding(top = TangemTheme.dimens.spacing14) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE), ) val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() @@ -66,7 +69,8 @@ internal fun LazyListScope.amountField( color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2), + .padding(top = TangemTheme.dimens.spacing2) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT), ) } CurrencyIcon( diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 5f2d7ca019..b22f5341e4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.singleEvent +import com.tangem.core.ui.test.StakingSendScreenTestTags @Composable fun NavigationButtonsBlock( @@ -146,7 +148,8 @@ private fun PreviousButton(prevButton: NavigationButton?) { .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) .clickable(onClick = button.onClick) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON), ) } } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 0bd12e57fd..323153c119 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -77,7 +77,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt index 0efb5068d1..8f0331ca8b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.api.common.config +import com.tangem.datasource.BuildConfig import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.ProviderSuspend @@ -14,20 +15,44 @@ internal class StakeKit( private val stakeKitAuthProvider: StakeKitAuthProvider, ) : ApiConfig() { - override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() override val environmentConfigs: List = listOf( createProdEnvironment(), + createMockEnvironment(), ) + private fun getInitialEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + MOCKED_BUILD_TYPE, + -> ApiEnvironment.MOCK + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + private fun createProdEnvironment(): ApiEnvironmentConfig { return ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://api.stakek.it/v1/", - headers = mapOf( - "X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey), - "accept" to ProviderSuspend { "application/json" }, - ), + headers = createHeaders(), ) } + + private fun createMockEnvironment(): ApiEnvironmentConfig { + return ApiEnvironmentConfig( + environment = ApiEnvironment.MOCK, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + } + + private fun createHeaders() = buildMap { + put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey)) + put(key = "accept", value = ProviderSuspend { "application/json" }) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt index ee3a3cffd6..aa2adc0817 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -7,7 +7,6 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig -import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager @@ -111,7 +110,7 @@ internal class RetrofitApiBuilder @Inject constructor( apiConfigId: ApiConfig.ID, environmentConfig: ApiEnvironmentConfig, ): OkHttpClient.Builder { - return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { + return if (BuildConfig.TESTER_MENU_ENABLED) { addInterceptor( interceptor = SwitchEnvironmentInterceptor( id = apiConfigId, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index e4d65b95dd..2219e2642d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation @@ -23,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -102,7 +104,9 @@ fun AmountTextField( singleLine = true, readOnly = !isEnabled, visualTransformation = visualTransformation, - modifier = Modifier.background(backgroundColor), + modifier = Modifier + .background(backgroundColor) + .testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 2d0c1b4fb1..72111e3c58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.SwapTokenScreenTestTags +import com.tangem.core.ui.test.BaseBlockTestTags /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -67,20 +67,23 @@ fun InputRowDefault( Column( modifier = Modifier .weight(1f) - .testTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK), + .testTag(BaseBlockTestTags.BLOCK), ) { title?.let { Text( text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = titleColor, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing8) + .testTag(BaseBlockTestTags.BLOCK_TITLE), ) } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, + modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT), ) } iconRes?.let { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index 942e60deaf..9de699245f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags @Suppress("LongParameterList") @Composable @@ -54,7 +56,8 @@ fun RoundableCornersRow( .padding( horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12, - ), + ) + .testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -63,6 +66,7 @@ fun RoundableCornersRow( color = startTextColor, maxLines = 1, style = startTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME), ) if (iconResId != null && iconClick != null) { Icon( @@ -85,6 +89,7 @@ fun RoundableCornersRow( color = endTextColor, maxLines = 1, style = endTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt new file mode 100644 index 0000000000..b2e4eb5d56 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseBlockTestTags { + const val BLOCK = "BASE_BLOCK" + const val BLOCK_TITLE = "BASE_BLOCK_TITLE" + const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt new file mode 100644 index 0000000000..f368755c2f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.test + +object StakingDetailsScreenTestTags { + const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + + const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE" + const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT" + + const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK" + const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME" + const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE" + const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT" + + const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt new file mode 100644 index 0000000000..138b27e35b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object StakingSendDetailsScreenTestTags { + + const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" + const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" + + const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" + const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt new file mode 100644 index 0000000000..1dc36d3f79 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object StakingSendScreenTestTags { + const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER" + + const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE" + const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT" + const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD" + const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT" + + const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON" + const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON" + const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON" + const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON" + const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index d8be9a8e43..cd6cf947d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -6,7 +6,6 @@ object SwapTokenScreenTestTags { const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" - const val NETWORK_FEE_BLOCK = "SWAP_TOKEN_SCREEN_NETWORK_FEE_BLOCK" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index f337c0bbc9..bcbee0ae13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -2,7 +2,19 @@ package com.tangem.core.ui.test object TokenDetailsScreenTestTags { const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE" const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON" const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS" + + const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK" + const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK" + const val STAKING_CURRENCY_ICON = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_CURRENCY_ICON" + const val STAKING_SERVICE_TITLE = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TITLE" + const val STAKING_SERVICE_TEXT = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TEXT" + const val STAKING_FIAT_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_FIAT_AMOUNT" + const val STAKING_DOT = "TOKEN_DETAILS_SCREEN_STAKING_DOT" + const val STAKING_TOKEN_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_TOKEN_AMOUNT" + const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE" + const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON" } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 462c5d22c0..27ba05eb91 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -42,6 +43,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType import com.tangem.features.staking.impl.R @@ -171,7 +173,8 @@ private fun LazyListScope.activeStakingBlock( currentIndex = index + 1, lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, - ), + ) + .testTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK), ) } } @@ -189,7 +192,9 @@ private fun BannerBlock(onClick: () -> Unit) { ), ) { Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(StakingDetailsScreenTestTags.BANNER_IMAGE), contentScale = ContentScale.FillWidth, painter = painterResource(R.drawable.img_staking_banner), contentDescription = null, @@ -197,7 +202,8 @@ private fun BannerBlock(onClick: () -> Unit) { Text( modifier = Modifier .align(Alignment.CenterStart) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .testTag(StakingDetailsScreenTestTags.BANNER_TEXT), text = buildAnnotatedString { withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { append(stringResourceSafe(R.string.staking_details_banner_text)) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 9cc528d859..e1a7c81bd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig @@ -20,6 +21,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep @@ -44,7 +46,8 @@ internal fun StakingScreen(uiState: StakingUiState) { .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(StakingSendScreenTestTags.SCREEN_CONTAINER), horizontalAlignment = Alignment.CenterHorizontally, ) { StakingAppBar( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt index c7e67f5415..b09a9ab029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -2,11 +2,14 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.features.staking.impl.R private const val TERMS_OF_USE_KEY = "termsOfUse" @@ -58,5 +61,6 @@ internal fun StakingTosText(onTextClick: (String) -> Unit) { onTextClick(PRIVACY_POLICY_URL) } }, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.TOS_TEXT), ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 0ca152acfb..7efc5b644c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -9,6 +9,7 @@ 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.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -27,6 +28,7 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.utils.StringsSigns.DASH_SIGN @@ -39,7 +41,8 @@ internal fun StakingFeeBlock(feeState: FeeState) { .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK), ) { Text( text = stringResourceSafe(R.string.common_network_fee_title), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 6e2d13e5e0..7182fc7bf4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -10,11 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder @@ -35,7 +37,8 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, - ), + ) + .testTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK), ) { InputRowImageInfo( title = resourceReference(R.string.staking_validator), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index cd79642bbc..d077ce9d52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -17,6 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.features.tokendetails.impl.R @@ -30,7 +32,9 @@ internal fun StakingBalanceBlock( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_BLOCK), ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), @@ -47,16 +51,19 @@ internal fun StakingBalanceBlock( text = state.fiatValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT), ) Text( text = StringsSigns.DOT, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_DOT), ) Text( text = state.cryptoValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT), ) } if (state.rewardValue != TextReference.EMPTY) { @@ -64,6 +71,7 @@ internal fun StakingBalanceBlock( text = state.rewardValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_REWARD_VALUE), ) } } @@ -71,6 +79,7 @@ internal fun StakingBalanceBlock( painter = painterResource(id = R.drawable.ic_chevron_right_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON), ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 807664c039..39bfbef5ec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -23,6 +24,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock @@ -77,7 +79,9 @@ internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, @Composable private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifier: Modifier = Modifier) { Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK), ) { Row { val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { @@ -87,7 +91,8 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi modifier = Modifier .size(TangemTheme.dimens.size20) .clip(TangemTheme.shapes.roundedCorners8) - .align(Alignment.CenterVertically), + .align(Alignment.CenterVertically) + .testTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON), icon = state.iconState, alpha = alpha, colorFilter = colorFilter, @@ -98,6 +103,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) @@ -106,6 +112,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size8)) From ddb0957f2e94deeb66f867ad67a6332123ae0bdf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 18:41:42 +0000 Subject: [PATCH 138/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 57e50cf4ac..fb79857d96 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1148" +tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-518" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-461" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From ce878fdbe249f6376ef4c30abe20e5d9dc255ee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 13:02:50 +0500 Subject: [PATCH 139/165] Updated on 2026-08-14 --- app/build.gradle.kts | 4 +++ .../main/java/com/tangem/tap/MainActivity.kt | 12 ++++++- .../tangem/tap/routing/utils/ChildFactory.kt | 9 +++++ .../com/tangem/common/routing/AppRoute.kt | 3 ++ .../configs/feature_toggles_config.json | 4 +++ features/tangempay/details/api/.gitignore | 1 + .../tangempay/details/api/build.gradle.kts | 18 ++++++++++ .../tangempay/TangemPayFeatureToggles.kt | 5 +++ .../components/TangemPayDetailsComponent.kt | 10 ++++++ features/tangempay/details/impl/.gitignore | 1 + .../tangempay/details/impl/build.gradle.kts | 32 ++++++++++++++++++ .../DefaultTangemPayFeatureToggles.kt | 10 ++++++ .../DefaultTangemPayDetailsComponent.kt | 33 +++++++++++++++++++ .../di/TangemPayDetailsFeatureModule.kt | 20 +++++++++++ .../tangempay/di/TangemPayDetailsModule.kt | 21 ++++++++++++ features/tangempay/main/api/.gitignore | 1 + features/tangempay/main/api/build.gradle.kts | 18 ++++++++++ features/tangempay/main/impl/.gitignore | 1 + features/tangempay/main/impl/build.gradle.kts | 32 ++++++++++++++++++ settings.gradle.kts | 6 ++++ 20 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 features/tangempay/details/api/.gitignore create mode 100644 features/tangempay/details/api/build.gradle.kts create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt create mode 100644 features/tangempay/details/impl/.gitignore create mode 100644 features/tangempay/details/impl/build.gradle.kts create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt create mode 100644 features/tangempay/main/api/.gitignore create mode 100644 features/tangempay/main/api/build.gradle.kts create mode 100644 features/tangempay/main/impl/.gitignore create mode 100644 features/tangempay/main/impl/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4de1a30e45..dd1295ea13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -238,6 +238,10 @@ dependencies { implementation(projects.features.home.impl) implementation(projects.features.account.api) implementation(projects.features.account.impl) + implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.details.impl) + implementation(projects.features.tangempay.main.api) + implementation(projects.features.tangempay.main.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2f037143f3..9cb6b6cd62 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -52,6 +52,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper @@ -197,6 +198,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + @Inject + internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -518,7 +522,13 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) - if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { + + // Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs + if (tangemPayFeatureToggles.isTangemPayEnabled) { + store.dispatchNavigationAction { + replaceAll(AppRoute.TangemPayDetails) + } + } else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { replaceAll( AppRoute.Welcome( diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 49bb638a53..40e0686f77 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -39,6 +39,7 @@ import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.v2.api.SendWithSwapComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -108,6 +109,7 @@ internal class ChildFactory @Inject constructor( private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, + private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -573,6 +575,13 @@ internal class ChildFactory @Inject constructor( componentFactory = archivedAccountListComponentFactory, ) } + is AppRoute.TangemPayDetails -> { + createComponentChild( + context = context, + params = TangemPayDetailsComponent.Params(), + componentFactory = tangemPayDetailsComponentFactory, + ) + } } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f797561ced..6d4e985501 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -353,4 +353,7 @@ sealed class AppRoute(val path: String) : Route { data class ArchivedAccountList( val userWalletId: UserWalletId, ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") + + @Serializable + data object TangemPayDetails : AppRoute(path = "/tangem_pay_details") } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 4cd82bc21e..0d8eea380b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -54,5 +54,9 @@ { "name": "NFT_SEND_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "TANGEM_PAY_ENABLED", + "version": "undefined" } ] diff --git a/features/tangempay/details/api/.gitignore b/features/tangempay/details/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/api/build.gradle.kts b/features/tangempay/details/api/build.gradle.kts new file mode 100644 index 0000000000..77acdd5c22 --- /dev/null +++ b/features/tangempay/details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt new file mode 100644 index 0000000000..393e589bce --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tangempay + +interface TangemPayFeatureToggles { + val isTangemPayEnabled: Boolean +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt new file mode 100644 index 0000000000..64e11cecbe --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayDetailsComponent : ComposableContentComponent { + @Suppress("EmptyDefaultConstructor") // Will add params in Next PRs + class Params() + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/.gitignore b/features/tangempay/details/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts new file mode 100644 index 0000000000..4f9fb1c110 --- /dev/null +++ b/features/tangempay/details/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt new file mode 100644 index 0000000000..a51c11a3bc --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTangemPayFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TangemPayFeatureToggles { + override val isTangemPayEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt new file mode 100644 index 0000000000..2c55cb6a3a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.decompose.context.AppComponentContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: TangemPayDetailsComponent.Params, +) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { + + @Composable + override fun Content(modifier: Modifier) { + Box(modifier.fillMaxSize().background(Color.Red)) + // TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : TangemPayDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayDetailsComponent.Params, + ): DefaultTangemPayDetailsComponent + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt new file mode 100644 index 0000000000..4cd92fc806 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.components.DefaultTangemPayDetailsComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayDetailsFeatureModule { + + @Binds + @Singleton + fun bindTangemPayDetailsComponentFactory( + factory: DefaultTangemPayDetailsComponent.Factory, + ): TangemPayDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt new file mode 100644 index 0000000000..a6ea142d28 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemPayDetailsModule { + + @Provides + @Singleton + fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { + return DefaultTangemPayFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tangempay/main/api/.gitignore b/features/tangempay/main/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts new file mode 100644 index 0000000000..15fb515b8b --- /dev/null +++ b/features/tangempay/main/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/main/impl/.gitignore b/features/tangempay/main/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts new file mode 100644 index 0000000000..eb442c8f69 --- /dev/null +++ b/features/tangempay/main/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b77ede786a..35d08f3116 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -264,6 +264,12 @@ include(":features:kyc:api") //TODO disable for release because of the permissions // include(":features:kyc:impl") +include(":features:tangempay:main:api") +include(":features:tangempay:main:impl") + +include(":features:tangempay:details:api") +include(":features:tangempay:details:impl") + include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") From 33fd5a927acf0443a29bc887276c11557a907884 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 11:01:21 +0200 Subject: [PATCH 140/165] Updated on 2026-08-14 --- features/markets/impl/build.gradle.kts | 2 + features/nft/impl/build.gradle.kts | 3 + features/onboarding-v2/impl/build.gradle.kts | 2 + .../tokenreceive/TokenReceiveComponent.kt | 18 ++ features/token-recieve/impl/build.gradle.kts | 1 + .../component/DefaultTokenReceiveComponent.kt | 197 ++++++++++++++++++ .../component/TokenReceiveAssetsComponent.kt | 43 ++++ .../component/TokenReceiveQrCodeComponent.kt | 39 ++++ .../component/TokenReceiveWarningComponent.kt | 38 ++++ .../tokenreceive/di/TokenReceiveModule.kt | 68 ++++++ .../model/TokenReceiveAssetsModel.kt | 35 ++++ .../tokenreceive/model/TokenReceiveModel.kt | 186 +++++++++++++++++ .../model/TokenReceiveQrCodeModel.kt | 35 ++++ .../model/TokenReceiveWarningModel.kt | 33 +++ .../tokenreceive/route/TokenReceiveRoutes.kt | 19 ++ .../ui/TokenReceiveQrCodeContent.kt | 2 +- .../tokenreceive/ui/state/QrCodeUM.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 3 + .../DefaultTokenDetailsComponent.kt | 29 +++ .../tokendetails/model/TokenDetailsModel.kt | 88 ++++++-- features/wallet/impl/build.gradle.kts | 2 + 21 files changed, 825 insertions(+), 20 deletions(-) create mode 100644 features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt create mode 100644 features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index df2755faef..1b9562b290 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api(projects.features.markets.api) api(projects.features.onramp.api) api(projects.features.sendV2.api) + api(projects.features.tokenRecieve.api) /* Data */ implementation(projects.data.common) @@ -39,6 +40,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.notifications.models) + implementation(projects.domain.transaction) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 0e4b9f5c67..438a815cc3 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.nft.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -34,6 +35,8 @@ dependencies { implementation(projects.domain.nft.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction) + implementation(projects.domain.tokens) /** Common */ implementation(projects.common.ui) diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 9653f895cb..d6abeb7356 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.features.manageTokens.api) implementation(projects.features.biometry.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.tokenRecieve.api) /** Core modules */ implementation(projects.core.configToggles) @@ -49,6 +50,7 @@ dependencies { implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.onramp) + implementation(projects.domain.transaction) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt new file mode 100644 index 0000000000..c37becdbb2 --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.tokenreceive + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig + +interface TokenReceiveComponent : ComposableBottomSheetComponent { + + data class Params( + val config: TokenReceiveConfig, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index dfd0d93ada..b1b8d7b048 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) /** Feature Apis */ implementation(projects.features.tokenRecieve.api) diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt new file mode 100644 index 0000000000..291ad7b029 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -0,0 +1,197 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.TokenReceiveContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultTokenReceiveComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: TokenReceiveComponent.Params, +) : TokenReceiveComponent, AppComponentContext by appComponentContext { + + private val model: TokenReceiveModel = getOrCreateModel(params) + private val innerRouter = InnerRouter( + stackNavigation = model.stackNavigation, + popCallback = { onChildBack() }, + ) + + private val contentStack = childStack( + key = "TokenReceiveFlowStack", + source = model.stackNavigation, + serializer = TokenReceiveRoutes.serializer(), + initialConfiguration = getInitialConfig(model.params.config.shouldShowWarning), + handleBackButton = false, + childFactory = ::screenChild, + ) + + @Composable + override fun BottomSheet() { + val content by contentStack.subscribeAsState() + val currentRoute = content.active.configuration + + TokenReceiveContentSheet( + route = currentRoute, + onCloseClick = ::dismiss, + onBackClick = ::onChildBack, + contentStack = content, + ) + } + + override fun dismiss() { + model.params.onDismiss() + } + + private fun onChildBack() { + when (contentStack.value.active.configuration) { + is TokenReceiveRoutes.QrCode -> model.stackNavigation.pop() + TokenReceiveRoutes.ReceiveAssets, + TokenReceiveRoutes.Warning, + -> dismiss() + } + } + + private fun screenChild( + config: TokenReceiveRoutes, + componentContext: ComponentContext, + ): ComposableContentComponent { + val appComponentContext = childByContext( + componentContext = componentContext, + router = innerRouter, + ) + return when (config) { + is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent( + appComponentContext = appComponentContext, + params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams( + network = model.params.config.cryptoCurrency.network.name, + address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"), + callback = model, + onDismiss = ::dismiss, + id = config.addressId, + ), + ) + TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent( + appComponentContext = appComponentContext, + params = TokenReceiveAssetsComponent.TokenReceiveAssetsParams( + addresses = model.state.value.addresses, + callback = model, + onDismiss = ::dismiss, + notificationConfigs = model.state.value.notificationConfigs, + showMemoDisclaimer = model.params.config.showMemoDisclaimer, + fullName = model.params.config.cryptoCurrency.network.name, + ), + ) + TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent( + appComponentContext = appComponentContext, + params = TokenReceiveWarningComponent.TokenReceiveWarningParams( + iconState = model.state.value.iconState, + callback = model, + onDismiss = ::dismiss, + network = model.params.config.cryptoCurrency.network, + ), + ) + } + } + + private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes { + return if (shouldShowWarning) { + TokenReceiveRoutes.Warning + } else { + TokenReceiveRoutes.ReceiveAssets + } + } + + @AssistedFactory + interface Factory : TokenReceiveComponent.Factory { + override fun create( + context: AppComponentContext, + params: TokenReceiveComponent.Params, + ): DefaultTokenReceiveComponent + } +} + +@Composable +internal fun TokenReceiveContentSheet( + route: TokenReceiveRoutes, + onCloseClick: () -> Unit, + onBackClick: () -> Unit, + contentStack: ChildStack, +) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBackClick, + containerColor = TangemTheme.colors.background.tertiary, + title = { + Title( + route = route, + onBackClick = onBackClick, + onCloseClick = onCloseClick, + ) + }, + content = { + TokenReceiveContent( + stackState = contentStack, + modifier = Modifier, + ) + }, + ) +} + +@Composable +private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { + when (route) { + is TokenReceiveRoutes.QrCode -> { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.ReceiveAssets -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.domain_receive_assets_navigation_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.Warning -> { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + } +} + +internal interface TokenReceiveModelCallback : + TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback, + TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback, + TokenReceiveWarningComponent.TokenReceiveWarningModelCallback \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt new file mode 100644 index 0000000000..de8c7e2578 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap + +internal class TokenReceiveAssetsComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveAssetsParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveAssetsContent(assetsUM = state) + } + + internal interface TokenReceiveAssetsModelCallback { + fun onQrCodeClick(id: Int) + fun onCopyClick(id: Int) + } + + data class TokenReceiveAssetsParams( + val notificationConfigs: ImmutableList, + val addresses: ImmutableMap, + val callback: TokenReceiveAssetsModelCallback, + val onDismiss: () -> Unit, + val showMemoDisclaimer: Boolean, + val fullName: String, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt new file mode 100644 index 0000000000..8191f9a208 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveQrCodeComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent + +internal class TokenReceiveQrCodeComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveQrCodeParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveQrCodeModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveQrCodeContent(qrCodeUM = state) + } + + internal interface TokenReceiveQrCodeModelCallback { + fun onCopyClick(id: Int) + fun onShareClick(address: String) + } + + data class TokenReceiveQrCodeParams( + val id: Int, + val network: String, + val address: ReceiveAddress, + val callback: TokenReceiveQrCodeModelCallback, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt new file mode 100644 index 0000000000..46ce303258 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveWarningComponent.kt @@ -0,0 +1,38 @@ +package com.tangem.features.tokenreceive.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.network.Network +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +import com.tangem.features.tokenreceive.ui.TokenReceiveWarningContent + +internal class TokenReceiveWarningComponent( + appComponentContext: AppComponentContext, + private val params: TokenReceiveWarningParams, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TokenReceiveWarningModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + TokenReceiveWarningContent(warningUM = state) + } + + internal interface TokenReceiveWarningModelCallback { + fun onWarningAcknowledged() + } + + data class TokenReceiveWarningParams( + val iconState: CurrencyIconState, + val callback: TokenReceiveWarningModelCallback, + val onDismiss: () -> Unit, + val network: Network, + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt new file mode 100644 index 0000000000..44cfdf6d8b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/di/TokenReceiveModule.kt @@ -0,0 +1,68 @@ +package com.tangem.features.tokenreceive.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.tokenreceive.DefaultTokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.tokenreceive.component.DefaultTokenReceiveComponent +import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel +import com.tangem.features.tokenreceive.model.TokenReceiveModel +import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel +import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeatureToggleModule { + + @Provides + @Singleton + fun provideTokenReceiveFeatureToggle(featureTogglesManager: FeatureTogglesManager): TokenReceiveFeatureToggle { + return DefaultTokenReceiveFeatureToggle( + featureTogglesManager = featureTogglesManager, + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultTokenReceiveComponent.Factory): TokenReceiveComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(TokenReceiveModel::class) + fun bindsTokenReceiveModel(model: TokenReceiveModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveAssetsModel::class) + fun bindsTokenReceiveAssetsModel(model: TokenReceiveAssetsModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveQrCodeModel::class) + fun bindsTokenReceiveQrCodeModel(model: TokenReceiveQrCodeModel): Model + + @Binds + @IntoMap + @ClassKey(TokenReceiveWarningModel::class) + fun bindsTokenReceiveWarningModel(model: TokenReceiveWarningModel): Model +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt new file mode 100644 index 0000000000..04b0caf856 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent +import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveAssetsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + ReceiveAssetsUM( + onCopyClick = params.callback::onCopyClick, + onOpenQrCodeClick = params.callback::onQrCodeClick, + addresses = params.addresses, + showMemoDisclaimer = params.showMemoDisclaimer, + isEnsResultLoading = false, + notificationConfigs = params.notificationConfigs, + fullName = params.fullName, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt new file mode 100644 index 0000000000..3313bf1cb9 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt @@ -0,0 +1,186 @@ +package com.tangem.features.tokenreceive.model + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.push +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.ui.R +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.notifications.NotificationConfig +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.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.ens.EnsAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase +import com.tangem.features.tokenreceive.TokenReceiveComponent +import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@ModelScoped +internal class TokenReceiveModel @Inject constructor( + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, + private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase, + private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model(), TokenReceiveModelCallback { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + val params = paramsContainer.require() + + val stackNavigation = StackNavigation() + + internal val state: StateFlow + field = MutableStateFlow(getInitState()) + + init { + modelScope.launch { + load() + } + } + + override fun onQrCodeClick(id: Int) { + stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id)) + } + + override fun onCopyClick(id: Int) { + val addressToCopy = state.value.addresses[id] ?: return + clipboardManager.setText(text = addressToCopy.value, isSensitive = true) + } + + override fun onShareClick(address: String) { + shareManager.shareText(text = address) + } + + override fun onWarningAcknowledged() { + modelScope.launch { + saveViewedTokenReceiveWarningUseCase.invoke( + when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.name + Asset.NFT -> asset.name + }, + ) + stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets) + } + } + + private fun mapAddresses(addresses: List): ImmutableMap { + return buildMap { + addresses.mapIndexed { index, model -> + val type = when (model.nameService) { + ReceiveAddressModel.NameService.Default -> { + ReceiveAddress.Type.Default( + displayName = stringReference(model.displayName), + ) + } + ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens + } + put( + key = index, + value = ReceiveAddress( + value = model.value, + type = type, + ), + ) + } + }.toPersistentMap() + } + + private suspend fun load() = withContext(dispatchers.default) { + state.value = state.value.copy( + isEnsResultLoading = true, + ) + + val reverseResolveResult = + if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) { + getReverseResolvedEnsAddressUseCase( + userWalletId = params.config.userWalletId, + network = params.config.cryptoCurrency.network, + addresses = params.config.receiveAddress.map { it.value }, + ) + } else { + emptyList() + } + + val currentAddressValues = state.value.addresses.values.map { it.value }.toSet() + + val newEnsAddresses = reverseResolveResult + .filterIsInstance() + .filterNot { it.name in currentAddressValues } + .map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) } + + val combinedAddresses = (state.value.addresses.values + newEnsAddresses) + .sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens }) + + val updatedAddresses = combinedAddresses + .mapIndexed { index, address -> index to address } + .toMap() + .toPersistentMap() + + state.value = state.value.copy( + isEnsResultLoading = false, + addresses = updatedAddresses, + ) + } + + private fun getNotifications(): List { + return buildList { + add( + NotificationUM.Info( + title = resourceReference( + R.string.receive_bottom_sheet_warning_title, + wrappedList( + when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.symbol + Asset.NFT -> asset.name + }, + params.config.cryptoCurrency.network.name, + ), + ), + subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), + iconTint = NotificationConfig.IconTint.Accent, + ), + ) + + params.config.tokenReceiveNotification.map { notification -> + add( + NotificationUM.Warning( + title = resourceReference(notification.title), + subtitle = resourceReference(notification.subtitle), + ), + ) + } + } + } + + private fun getInitState(): TokenReceiveUM { + return TokenReceiveUM( + addresses = mapAddresses(params.config.receiveAddress), + iconState = iconStateConverter.convert(params.config.cryptoCurrency), + network = params.config.cryptoCurrency.network.name, + isEnsResultLoading = false, + notificationConfigs = getNotifications().toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt new file mode 100644 index 0000000000..e7b1915b1b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress +import com.tangem.features.tokenreceive.ui.state.QrCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveQrCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + QrCodeUM( + network = params.network, + addressValue = params.address.value, + addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: stringReference(""), + onCopyClick = { params.callback.onCopyClick(params.id) }, + onShareClick = params.callback::onShareClick, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt new file mode 100644 index 0000000000..8c686c07a7 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveWarningModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tokenreceive.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.iconResId +import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent +import com.tangem.features.tokenreceive.ui.state.WarningUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TokenReceiveWarningModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val state: StateFlow + field = MutableStateFlow( + WarningUM( + iconState = params.iconState, + onWarningAcknowledged = params.callback::onWarningAcknowledged, + network = params.network.name, + networkIcon = params.network.iconResId, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt new file mode 100644 index 0000000000..c98a6e3c12 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/route/TokenReceiveRoutes.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tokenreceive.route + +import androidx.compose.runtime.Immutable +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Immutable +@Serializable +internal sealed interface TokenReceiveRoutes : Route { + + @Serializable + data object Warning : TokenReceiveRoutes + + @Serializable + data object ReceiveAssets : TokenReceiveRoutes + + @Serializable + data class QrCode(val addressId: Int) : TokenReceiveRoutes +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt index 2eb6310cbd..d248c1b34d 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt @@ -60,7 +60,7 @@ internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) { Buttons( onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) }, - onCopyClick = { qrCodeUM.onCopyClick(qrCodeUM.addressValue) }, + onCopyClick = qrCodeUM.onCopyClick, snackbarHostState = snackbarHostState, ) } diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt index e3c4f5adf0..274735da2a 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.tokenreceive.ui.state import com.tangem.core.ui.extensions.TextReference internal data class QrCodeUM( - val onCopyClick: (String) -> Unit, + val onCopyClick: (Int) -> Unit, val onShareClick: (String) -> Unit, val addressName: TextReference, val addressValue: String, diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4bcd8ce768..5da95d55a3 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -101,5 +101,8 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.txhistory.api) implementation(projects.features.sendV2.api) + implementation(projects.features.tokenRecieve.api) + + implementation(deps.decompose.ext.compose) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 410b089abe..2291d82965 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -4,16 +4,24 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.txhistory.component.TxHistoryComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Assisted params: TokenDetailsComponent.Params, tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory, txHistoryComponentFactory: TxHistoryComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { private val model: TokenDetailsModel = getOrCreateModel(params) @@ -37,6 +46,13 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + init { lifecycle.subscribe( onPause = model::onPause, @@ -54,12 +70,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NavigationBar3ButtonsScrim() TokenDetailsScreen( state = state, tokenMarketBlockComponent = tokenMarketBlockComponent, txHistoryComponent = txHistoryComponent, ) + bottomSheet.child?.instance?.BottomSheet() } private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? { @@ -68,6 +86,17 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( return TokenMarketBlockComponent.Params(cryptoCurrency = this) } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : TokenDetailsComponent.Factory { override fun create( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f5352430b4..7f2de4ef2c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable import androidx.paging.cachedIn import arrow.core.getOrElse import arrow.core.merge +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -35,8 +37,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -60,10 +65,7 @@ import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.IncompleteTransactionError import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.AssociateAssetUseCase -import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase -import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase -import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -82,6 +84,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -137,6 +140,9 @@ internal class TokenDetailsModel @Inject constructor( private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val sendFeatureToggles: SendFeatureToggles, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model(), TokenDetailsClickIntents { private val params = paramsContainer.require() @@ -159,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor( /** Transaction id to check for status */ private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), @@ -594,20 +602,27 @@ internal class TokenDetailsModel @Inject constructor( } modelScope.launch { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) - - internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( - currency = cryptoCurrency, - networkAddress = networkAddress, - onCopyClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) - shareManager.shareText(text = it) - }, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses(addresses = networkAddress), + ) + } else { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) + internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( + currency = cryptoCurrency, + networkAddress = networkAddress, + onCopyClick = { + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + analyticsEventsHandler.send( + TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), + ) + shareManager.shareText(text = it) + }, + ) + } } } @@ -1061,6 +1076,43 @@ internal class TokenDetailsModel @Inject constructor( .launchIn(modelScope) } + private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig { + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e6d1f16109..a71f5825e3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.notifications) implementation(projects.domain.notifications.toggles) + implementation(projects.domain.transaction) /** Feature Apis */ implementation(projects.features.details.api) @@ -113,6 +114,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.tokenRecieve.api) /** Common modules */ implementation(projects.common) From 18fe9257cb2910be6cde21d58196c04b461f12ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 12:08:16 +0300 Subject: [PATCH 141/165] Updated on 2026-08-14 --- .../impl/DefaultOnboardingNoteComponent.kt | 13 +- .../topup/OnboardingNoteTopUpComponent.kt | 39 --- .../topup/model/OnboardingNoteTopUpModel.kt | 253 ------------------ .../topup/ui/OnboardingNoteTopUpHeader.kt | 102 ------- .../topup/ui/OnboardingNoteTopUpScreen.kt | 130 --------- .../topup/ui/state/OnboardingNoteTopUpUM.kt | 19 -- .../v2/note/impl/di/ComponentModule.kt | 6 - .../v2/note/impl/model/OnboardingNoteModel.kt | 2 +- .../v2/note/impl/route/OnboardingNoteRoute.kt | 4 +- .../onboarding/v2/note/impl/route/Step.kt | 2 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 167 +----------- .../v2/twin/impl/ui/OnboardingTwin.kt | 70 ----- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 103 +------ .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 21 -- 14 files changed, 23 insertions(+), 908 deletions(-) delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index bea41ef278..7f15990f3c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -98,14 +99,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onWalletCreated = { userWallet -> model.onWalletCreated(userWallet) - model.stackNavigation.push(OnboardingNoteRoute.TopUp) + model.stackNavigation.push(OnboardingNoteRoute.Done) }, ), ) - OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( - appComponentContext = factoryContext, - params = OnboardingNoteTopUpComponent.Params( - childParams = childParams, + OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create( + context = factoryContext, + params = OnboardingDoneComponent.Params( + mode = OnboardingDoneComponent.Mode.WalletCreated, onDone = { params.onDone() }, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt deleted file mode 100644 index 5b9cc4f1af..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp - -internal class OnboardingNoteTopUpComponent( - appComponentContext: AppComponentContext, - private val params: Params, -) : ComposableContentComponent, AppComponentContext by appComponentContext { - - private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) - - OnboardingNoteTopUp( - modifier = modifier, - state = state, - ) - } - - data class Params( - val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt deleted file mode 100644 index 4f0a65a810..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.model - -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.extensions.isPositive -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -@ModelScoped -internal class OnboardingNoteTopUpModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val rampStateManager: RampStateManager, - private val cardRepository: CardRepository, - private val saveWalletUseCase: SaveWalletUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, -) : Model() { - - private val params = paramsContainer.require() - private val commonState = params.childParams.commonState - private val scanResponse = params.childParams.commonState.value.scanResponse - private var userWallet = params.childParams.commonState.value.userWallet - - private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM( - onRefreshBalanceClick = ::refreshBalance, - onBuyCryptoClick = ::onBuyCryptoClick, - onShowWalletAddressClick = ::onShowWalletAddressClick, - onDismissBottomSheet = ::onDismissBottomSheet, - ), - ) - - val uiState: StateFlow = _uiState - - init { - Analytics.send(OnboardingEvent.Topup.ScreenOpened) - observeArtwork() - modelScope.launch { - createUserWalletIfNull() - cardRepository.finishCardActivation(scanResponse.card.cardId) - observeCryptoCurrencyStatus() - refreshBalance() - } - } - - private fun refreshBalance() { - modelScope.launch { - showBalanceLoadingProgress(true) - createUserWalletIfNull() - val userWalletId = requireNotNull(userWallet?.walletId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true) - } - showBalanceLoadingProgress(false) - } - } - - private fun onBuyCryptoClick() { - val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - modelScope.launch { - getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { - urlOpener.openUrl(it) - } - } - Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) - } - - private fun onShowWalletAddressClick() { - val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - val networkAddress = currencyStatus.value.networkAddress ?: return - - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) - } - Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) - } - - private fun onDismissBottomSheet() { - _uiState.update { - it.copy(addressBottomSheetConfig = null) - } - } - - private suspend fun createUserWalletIfNull() { - if (userWallet != null) { - return - } - val commonState = params.childParams.commonState.value - userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) - } - - private fun observeArtwork() { - modelScope.launch { - params.childParams.commonState.collect { - _uiState.value = _uiState.value.copy( - cardArtwork = it.cardArtwork, - ) - } - } - } - - private fun observeCryptoCurrencyStatus() { - val userWalletId = userWallet?.walletId ?: return - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) - .map { it.getOrNull() } - .filterNotNull() - .onEach(::applyCryptoCurrencyStatusToState) - .launchIn(modelScope) - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - if (commonState.value.cryptoCurrencyStatus == null) { - loadAvailableForBuy(status) - } - - commonState.update { - it.copy(cryptoCurrencyStatus = status) - } - - val amount = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.amount - is CryptoCurrencyStatus.NoAccount -> status.value.amount - is CryptoCurrencyStatus.NoQuote -> status.value.amount - else -> null - } - val hasCurrentNetworkTransactions = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions - is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions - else -> false - } - val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount - - if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { - params.onDone() - } - - _uiState.update { - it.copy( - amountToCreateAccount = amountToCreateAccount - ?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }, - balance = amount?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }.orEmpty(), - isTopUpDataLoading = status.value.networkAddress == null, - ) - } - } - - private fun showBalanceLoadingProgress(value: Boolean) { - _uiState.update { - it.copy(isRefreshing = value) - } - } - - private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { - modelScope.launch { - val availableForBuy = rampStateManager.availableForBuy( - userWallet = userWallet ?: return@launch, - cryptoCurrency = cryptoCurrencyStatus.currency, - ) - _uiState.update { - it.copy( - availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None, - availableForBuyLoading = false, - ) - } - } - } - - private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = uiState.value.onDismissBottomSheet, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currencyStatus.currency.name, - symbol = currencyStatus.currency.symbol, - ), - network = currencyStatus.currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ) - - private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { - val wallet = requireNotNull( - value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(), - lazyMessage = { "User wallet not created" }, - ) - saveWalletUseCase(wallet, false) - return wallet - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt deleted file mode 100644 index 672d5e838b..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -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.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.LocalTangemShimmer -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.common.ui.RefreshButton -import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R -import com.valentinilk.shimmer.shimmer - -@Composable -fun OnboardingNoteTopUpHeader( - balance: String, - cardArtwork: ArtworkUM?, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .heightIn(min = 180.dp) - .widthIn(max = 450.dp), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 32.dp), - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - modifier = if (balance.isEmpty()) { - Modifier - .width(120.dp) - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) - .shimmer(LocalTangemShimmer.current) - } else { - Modifier - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - text = balance, - ) - SpacerHMax() - } - } - WalletCard( - modifier = Modifier.width(120.dp).align(Alignment.TopCenter), - artwork = cardArtwork, - ) - RefreshButton( - modifier = Modifier.align(Alignment.BottomCenter), - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardinNoteTopUpHeaderPreview() { - TangemThemePreview { - OnboardingNoteTopUpHeader( - balance = "0.00000001 BTC", - cardArtwork = ArtworkUM(null, ""), - onRefreshBalanceClick = {}, - isRefreshing = false, - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt deleted file mode 100644 index 476d06b378..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM - -@Composable -fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnboardingNoteTopUpHeader( - balance = state.balance, - cardArtwork = state.cardArtwork, - onRefreshBalanceClick = state.onRefreshBalanceClick, - isRefreshing = state.isRefreshing, - modifier = Modifier - .padding(top = 64.dp) - .padding(horizontal = 24.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth(), - ) - Column( - modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 16.dp), - ) - - val text = if (state.amountToCreateAccount != null) { - stringResourceSafe( - R.string.onboarding_top_up_min_create_account_amount, - state.amountToCreateAccount, - ) - } else { - stringResourceSafe(R.string.onboarding_top_up_body) - } - SpacerH16() - Text( - text = text, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - - BottomButtons(state) - - state.addressBottomSheetConfig?.let { config -> - TokenReceiveBottomSheet(config = config) - } - } -} - -@Composable -private fun BottomButtons(state: OnboardingNoteTopUpUM) { - if (state.availableForBuy) { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - } else { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_button_receive_crypto), - onClick = state.onShowWalletAddressClick, - ) - } - AnimatedVisibility(visible = !state.availableForBuyLoading) { - if (state.availableForBuy) { - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardingNoteTopUpPreview() { - TangemThemePreview { - OnboardingNoteTopUp( - state = OnboardingNoteTopUpUM( - availableForBuy = true, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt deleted file mode 100644 index c9d9ca3541..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig - -data class OnboardingNoteTopUpUM( - val cardArtwork: ArtworkUM? = null, - val availableForBuy: Boolean = false, - val availableForBuyLoading: Boolean = true, - val balance: String = "", - val isRefreshing: Boolean = false, - val isTopUpDataLoading: Boolean = true, - val amountToCreateAccount: String? = null, - val addressBottomSheetConfig: TangemBottomSheetConfig? = null, - val onBuyCryptoClick: () -> Unit = {}, - val onShowWalletAddressClick: () -> Unit = {}, - val onRefreshBalanceClick: () -> Unit = {}, - val onDismissBottomSheet: () -> Unit = {}, -) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt index be064600cc..136a2ae24d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import dagger.Binds import dagger.Module @@ -37,9 +36,4 @@ internal interface ModelModule { @IntoMap @ClassKey(OnboardingNoteCreateWalletModel::class) fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model - - @Binds - @IntoMap - @ClassKey(OnboardingNoteTopUpModel::class) - fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 5d165cbe97..ae47a8ab9d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor( return if (card.wallets.isEmpty()) { OnboardingNoteRoute.CreateWallet } else { - OnboardingNoteRoute.TopUp + OnboardingNoteRoute.Done } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt index 1d7573e677..cc8101af19 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt @@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute { data object CreateWallet : OnboardingNoteRoute() @Serializable - data object TopUp : OnboardingNoteRoute() + data object Done : OnboardingNoteRoute() } -internal const val ONBOARDING_NOTE_STEPS_COUNT = 3 \ No newline at end of file +internal const val ONBOARDING_NOTE_STEPS_COUNT = 2 \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt index 3df643e22b..83b41e6d8f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt @@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route internal fun OnboardingNoteRoute.stepNum() = when (this) { OnboardingNoteRoute.CreateWallet -> 1 - OnboardingNoteRoute.TopUp -> 2 + OnboardingNoteRoute.Done -> 2 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index db44e189c2..ab86ceed99 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -7,40 +7,22 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.datasource.local.config.issuers.IssuersConfigStorage -import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.DeleteWalletUseCase @@ -61,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -81,16 +61,8 @@ internal class OnboardingTwinModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val issuersConfigStorage: IssuersConfigStorage, private val cardRepository: CardRepository, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val tokensFeatureToggles: TokensFeatureToggles, - private val walletBalanceFetcher: WalletBalanceFetcher, ) : Model() { private val params = paramsContainer.require() @@ -116,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - if (params.scanResponse.twinsIsTwinned()) { - OnboardingTwinUM.TopUpPrepare - } else { - OnboardingTwinUM.Welcome( - pairCardNumber = firstCardTwinNumber.pairNumber().number, - onContinueClick = ::navigateToFirstScan, - ) - } + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) } }, ) @@ -139,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor( saveTwinsOnboardingShownUseCase() } } - OnboardingTwinUM.TopUpPrepare -> { - modelScope.launch { - setTopUpState(params.scanResponse) - } - } else -> {} } } @@ -218,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } - - innerNavigationState.update { - it.copy(stackSize = 2) - } + innerNavigationState.update { it.copy(stackSize = 2) } } } } @@ -229,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor( private fun createSecondWallet(firstPublicKey: String) { setLoading(true) - modelScope.launch { val secondCardNumber = firstCardTwinNumber.pairNumber().number val result = tangemSdkManager.createSecondTwinWallet( @@ -318,13 +277,13 @@ internal class OnboardingTwinModel @Inject constructor( Mode.CreateWallet -> { modelScope.launch { setLoading(true) - setTopUpState(scanResponse) + finishActivation(scanResponse) }.saveIn(cryptoCurrencyStatusJobHolder) } } } - private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope { + private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { Timber.e("User wallet not created") setLoading(false) @@ -342,117 +301,7 @@ internal class OnboardingTwinModel @Inject constructor( cardRepository.finishCardActivation(params.scanResponse.card.cardId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - } else { - fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) - } - - val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@coroutineScope - } - - launch { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .collect { - it.onRight { status -> - applyCryptoCurrencyStatusToState(status) - } - } - } - - _uiState.value = OnboardingTwinUM.TopUp( - onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, - onRefreshClick = { onRefreshBalanceClick(userWallet) }, - onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, - isLoading = true, - ) - - innerNavigationState.update { - it.copy(stackSize = 4) - } - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return - if (amount > BigDecimal.ZERO) { - params.modelCallbacks.onDone() - } else { - update { - it.copy( - balance = BigDecimal.ZERO.format { crypto(status.currency) }, - onBuyCryptoClick = { onBuyCryptoClick(status) }, - onShowAddressClick = { onShowAddressClick(status) }, - isLoading = false, - ) - } - } - } - - private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { - modelScope.launch { - getLegacyTopUpUrlUseCase(status).onRight { - urlOpener.openUrl(it) - } - } - } - - private fun onShowAddressClick(status: CryptoCurrencyStatus) { - val currency = status.currency - val networkAddress = status.value.networkAddress ?: return - - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, - ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ), - ) - } - } - - private fun onRefreshBalanceClick(userWallet: UserWallet) { - update { - it.copy(isLoading = true) - } - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true) - } - } + params.modelCallbacks.onDone() } private fun saveWalletAndDone() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index c67669148b..d2e579d459 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi .weight(.48f) .fillMaxWidth(), state = state.artwork, - balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", - isRefreshing = state.isLoading, - onRefreshBalanceClick = { - (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() - }, ) AnimatedContent( @@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi when (st) { is OnboardingTwinUM.ResetWarning -> ResetWarning(st) is OnboardingTwinUM.ScanCard -> ScanCard(st) - is OnboardingTwinUM.TopUp -> TopUp(st) is OnboardingTwinUM.Welcome -> Welcome(st) - OnboardingTwinUM.TopUpPrepare -> {} } } } - - if (state is OnboardingTwinUM.TopUp) { - TokenReceiveBottomSheet(config = state.bottomSheetConfig) - } } @Suppress("LongMethod") @@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie } } -@Composable -private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Column( - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - SpacerH16() - - Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body1, - ) - } - - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowAddressClick, - ) - } -} - @Composable private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { Column( @@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi } } -@Preview(showBackground = true) -@Composable -private fun PreviewTopUp() { - TangemThemePreview { - OnboardingTwin(OnboardingTwinUM.TopUp()) - } -} - @Preview(showBackground = true) @Composable private fun PreviewWelcome() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 97758be4e1..3e0240af22 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -1,12 +1,10 @@ package com.tangem.features.onboarding.v2.twin.impl.ui +import android.annotation.SuppressLint import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.wallets.models.Artwork -import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit @@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM { FirstCard, SecondCard } } - - data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -64,15 +54,10 @@ private data class WalletCardTransitionState( val zIndex: Float = 0f, ) +@SuppressLint("UnusedBoxWithConstraintsScope") @Suppress("LongMethod") @Composable -internal fun TwinWalletArtworks( - state: TwinWalletArtworkUM, - balance: String, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) { BoxWithConstraints( modifier .heightIn(min = 180.dp) @@ -110,22 +95,6 @@ internal fun TwinWalletArtworks( } } - AnimatedVisibility( - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - ) - } - AnimatedTwinCards( transition1 = transition1, transition2 = transition2, @@ -133,46 +102,6 @@ internal fun TwinWalletArtworks( .widthIn(max = 450.dp) .matchParentSize(), ) - - AnimatedVisibility( - modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = balance, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - } - - AnimatedVisibility( - modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - RefreshButton( - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } } } @@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState( ) } } - TwinWalletArtworkUM.TopUp -> { - val scale = 0.4f - val yTranslation = -maxHeightDp * density - 24 * density - listOf( - CardsTransitionState( - walletCard1 = WalletCardTransitionState( - yTranslation = yTranslation, - xScale = scale, - yScale = scale, - zIndex = 2f, - ), - walletCard2 = WalletCardTransitionState( - yTranslation = yTranslation * 0.35f, - xScale = scale * 0.8f, - yScale = scale * 0.8f, - zIndex = 1f, - ), - ), - ) - } } @Preview(showBackground = true, widthDp = 360, heightDp = 640) @@ -341,16 +250,13 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) } TwinWalletArtworks( state = state, modifier = Modifier .padding(top = 250.dp) .fillMaxWidth(), - balance = "1 USD", - isRefreshing = false, - onRefreshBalanceClick = {}, ) var index by remember { mutableIntStateOf(0) } @@ -366,7 +272,6 @@ private fun Preview() { TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), - TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 7341af8545..defa7831a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM @Immutable @@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM { abstract val isLoading: Boolean abstract val artwork: TwinWalletArtworkUM - data object TopUpPrepare : OnboardingTwinUM() { - override val stepIndex: Int = 0 - override val isLoading: Boolean = false - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread - } - data class Welcome( override val isLoading: Boolean = false, val pairCardNumber: Int = 2, @@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM { override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) } - data class TopUp( - override val isLoading: Boolean = false, - val balance: String = "", - val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, - val onBuyCryptoClick: () -> Unit = {}, - val onShowAddressClick: () -> Unit = {}, - val onRefreshClick: () -> Unit = {}, - ) : OnboardingTwinUM() { - override val stepIndex: Int = 2 - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp - } - fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { is Welcome -> copy(isLoading = isLoading) is ResetWarning -> copy() is ScanCard -> copy(isLoading = isLoading) - is TopUp -> copy(isLoading = isLoading) - TopUpPrepare -> this } } \ No newline at end of file From b9e992c5338ed285442a93e6da8c72022530c297 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 18:02:23 +0700 Subject: [PATCH 142/165] Updated on 2026-08-14 --- .../tangem/common/ui/account/AccountIcon.kt | 134 ++++++++++++++++++ .../tangem/common/ui/account/AccountRow.kt | 110 ++++++++++++++ .../ui/account/CryptoPortfolioIconExt.kt | 5 +- .../ui/account/CryptoPortfolioIconUM.kt | 7 +- .../archived/entity/AccountArchivedUM.kt | 6 +- .../archived/ui/ArchivedAccountListContent.kt | 42 ++---- .../createedit/AccountCreateEditModel.kt | 2 +- .../createedit/entity/AccountCreateEditUM.kt | 2 +- .../entity/AccountCreateEditUMBuilder.kt | 2 +- .../createedit/ui/AccountCreateEditContent.kt | 52 ++----- .../account/details/AccountDetailsModel.kt | 2 +- .../details/entity/AccountDetailsUM.kt | 2 +- .../details/ui/AccountDetailsContent.kt | 75 ++-------- 13 files changed, 296 insertions(+), 145 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt rename features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt => common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt (66%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt new file mode 100644 index 0000000000..8a441922f5 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt @@ -0,0 +1,134 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color + +enum class AccountIconSize { + Default, Large, Medium, Small, ExtraSmall +} + +/** + * Displays an account icon that can either show a letter (derived from [name]) + * or a predefined vector resource (from [icon]). + * + * The background color is determined by the icon's [CryptoPortfolioIconUM.color], + * and the icon size, text style, and box modifier are adapted based on the given [size]. + * + * @param name The text reference used to resolve and display the first letter + * when [icon] is set to [CryptoPortfolioIcon.Icon.Letter]. + * @param icon The account icon definition, which can be a letter or a drawable resource. + * @param size The size of the icon, defined by [AccountIconSize]. + */ +@Composable +fun AccountIcon( + name: TextReference, + icon: CryptoPortfolioIconUM, + size: AccountIconSize, + modifier: Modifier = Modifier, +) { + val boxModifier = modifier.selectBoxModifier(size) + val iconSize = Modifier.selectIconSize(size) + val textStyle = when (size) { + AccountIconSize.Default -> TangemTheme.typography.h3 + AccountIconSize.Large -> TangemTheme.typography.h1 + AccountIconSize.Medium -> TangemTheme.typography.subtitle1 + AccountIconSize.Small -> TangemTheme.typography.subtitle2 + AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1 + } + Box( + contentAlignment = Alignment.Center, + modifier = boxModifier.background(icon.color.getUiColor()), + ) { + val icon = icon.value + val letter = name.resolveReference().firstOrNull() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter?.uppercase() ?: "", + style = textStyle, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = iconSize, + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> this.size(20.dp) + AccountIconSize.Large -> this.size(40.dp) + AccountIconSize.Medium -> this.size(16.dp) + AccountIconSize.Small -> this.size(12.dp) + AccountIconSize.ExtraSmall -> this.size(8.dp) +} + +private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp)) + AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp)) + AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp)) + AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp)) + AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp)) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_AccountIcon() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Account Name") + Row( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall) + } + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall) + } + } +} + +object AccountIconPreviewData { + + fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM( + value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(), + color = Color.entries.random(), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt new file mode 100644 index 0000000000..c48fa40072 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -0,0 +1,110 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Displays a row representing an account with an icon, title, and subtitle. + * + * The row consists of: + * - An [AccountIcon] on the left. + * - A column with the [title] and [subtitle] texts, which can be displayed in normal + * or reversed order depending on [isReverse]. + * + * The layout uses horizontal spacing between the icon and text, and vertical spacing + * between the title and subtitle. + * + * @param title The main text shown in the row, usually representing the account name. + * @param subtitle The secondary text, typically providing additional details about the account. + * @param icon The account icon definition, displayed using [AccountIcon]. + * @param isReverse If `true`, the [subtitle] is displayed above the [title]. + * Otherwise, the [title] is displayed above the [subtitle]. + */ +@Composable +fun AccountRow( + title: TextReference, + subtitle: TextReference, + icon: CryptoPortfolioIconUM, + modifier: Modifier = Modifier, + isReverse: Boolean = false, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + name = title, + icon = icon, + size = AccountIconSize.Default, + ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + if (isReverse) { + Subtitle(subtitle) + Title(title) + } else { + Title(title) + Subtitle(subtitle) + } + } + } +} + +@Composable +private fun Title(title: TextReference) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) +} + +@Composable +private fun Subtitle(subtitle: TextReference) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = subtitle.resolveReference(), + ) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Main account") + val info = stringReference("10 tokens in 2 networks") + val subtitle = resourceReference(R.string.account_form_name) + fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter) + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + AccountRow(title = name, subtitle = info, icon = icon()) + AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt index 14015c86b2..d97401c0a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt @@ -47,4 +47,7 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int { CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24 CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24 } -} \ No newline at end of file +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt similarity index 66% rename from features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt rename to common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt index 299fb679dc..cb8b2d22af 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.account.common +package com.tangem.common.ui.account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.CryptoPortfolioIcon.Color @@ -12,7 +12,4 @@ data class CryptoPortfolioIconUM( value = domainModel.value, color = domainModel.color, ) -} - -fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this) -fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color) \ No newline at end of file +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt index ee42b871ff..bd72960062 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.account.archived.entity +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList internal sealed interface AccountArchivedUM { @@ -20,8 +20,8 @@ internal sealed interface AccountArchivedUM { internal data class ArchivedAccountUM( val accountId: String, - val accountName: String, - val accountIcon: CryptoPortfolioIconUM, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, val tokensInfo: TextReference, val onClick: (accountId: String) -> Unit, ) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt index 65b5e1794c..a563f952d3 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -6,33 +6,28 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text 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.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountRow import com.tangem.core.res.R import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.archived.entity.AccountArchivedUM import com.tangem.features.account.archived.entity.ArchivedAccountUM -import com.tangem.features.account.common.toUM -import com.tangem.features.account.details.ui.AccountIcon import kotlinx.collections.immutable.toImmutableList @Composable @@ -135,28 +130,12 @@ private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Mod verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - AccountIcon( - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(9.dp)), - accountName = item.accountName, - accountIcon = item.accountIcon, - ) - Column( + AccountRow( + title = item.accountName, + subtitle = item.tokensInfo, + icon = item.accountIconUM, modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = item.accountName, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - text = item.tokensInfo.resolveReference(), - ) - } + ) SecondarySmallButton( config = SmallButtonConfig( @@ -179,13 +158,14 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider:: @Suppress("MagicNumber") private class PreviewStateProvider : CollectionPreviewParameterProvider( buildList { - fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + fun portfolioIcon() = AccountIconPreviewData.randomAccountIcon() + val accountName = stringReference("Account name") val firstList = List(10) { ArchivedAccountUM( accountId = it.toString(), - accountName = "Account name", - accountIcon = portfolioIcon(), + accountName = accountName, + accountIconUM = portfolioIcon(), tokensInfo = stringReference("10 tokens in 2 networks"), onClick = {}, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 0f5a66dedf..af1c7d4ed1 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.account.createedit import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.common.ui.account.toDomain import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,7 +21,6 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index df93dde4b9..330fc2352f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList data class AccountCreateEditUM( diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index bacbd306ab..654515e302 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.toUM import com.tangem.core.res.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -7,7 +8,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList internal class AccountCreateEditUMBuilder( diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index b94e9198fe..639b44419e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -24,6 +24,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.R +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountIconSize import com.tangem.common.ui.account.getResId import com.tangem.common.ui.account.getUiColor import com.tangem.core.ui.components.PrimaryButton @@ -36,7 +39,6 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.toUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account import kotlinx.collections.immutable.toImmutableList @@ -69,7 +71,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi SpacerH24() AccountColor(state.colorsState) SpacerH24() - AccountIcon(state.iconsState) + AccountIcons(state.iconsState) SpacerH8() Text( modifier = Modifier.padding(horizontal = 8.dp), @@ -100,7 +102,11 @@ private fun AccountSummary(account: Account) { ) { Spacer(modifier = Modifier.height(24.dp)) - AccountIcon(account) + AccountIcon( + name = stringReference(account.name), + icon = account.portfolioIcon, + size = AccountIconSize.Large, + ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -122,34 +128,6 @@ private fun AccountSummary(account: Account) { } } -@Composable -private fun AccountIcon(account: Account) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(88.dp) - .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) - .background(account.portfolioIcon.color.getUiColor()), - ) { - val icon = account.portfolioIcon.value - val letter = account.name.firstOrNull() - ?: account.inputPlaceholder.resolveReference().first() - when { - icon == CryptoPortfolioIcon.Icon.Letter -> Text( - text = letter.uppercase(), - style = TangemTheme.typography.head, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(44.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Suppress("LongMethod", "MagicNumber") @Composable private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @@ -201,7 +179,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @Suppress("LongMethod", "MagicNumber") @Composable -private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { +private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { Box( Modifier .clip(RoundedCornerShape(16.dp)) @@ -296,7 +274,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Text( - text = letter.uppercase(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(20.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -217,20 +170,18 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider:: private class PreviewStateProvider : CollectionPreviewParameterProvider( buildList { - var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val accountName = "Main" + var portfolioIcon = AccountIconPreviewData.randomAccountIcon() val first = AccountDetailsUM( onCloseClick = {}, onAccountEditClick = {}, onManageTokensClick = {}, onArchiveAccountClick = {}, - accountName = "Main", + accountName = accountName, accountIcon = portfolioIcon, ) add(first) - portfolioIcon = portfolioIcon.copy( - value = CryptoPortfolioIcon.Icon.Letter, - color = CryptoPortfolioIcon.Color.entries.random(), - ) + portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true) add(first.copy(accountIcon = portfolioIcon)) }, ) \ No newline at end of file From 369f449c09cc0e53488abbf06fd233fe00a76985 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:09:31 +0500 Subject: [PATCH 143/165] Updated on 2026-08-14 --- .../bottomsheets/OptionsBottomSheet.kt | 99 +++++++++++++++++++ .../bottomsheets/OptionsBottomSheetContent.kt | 23 +++++ features/details/impl/build.gradle.kts | 1 + .../details/entity/UserWalletListUM.kt | 2 + .../details/model/UserWalletListModel.kt | 75 ++++++++++++-- .../details/ui/UserWalletListBlock.kt | 15 +++ features/welcome/impl/build.gradle.kts | 1 + .../welcome/impl/model/WelcomeModel.kt | 50 ++++++++-- .../welcome/impl/ui/AddWalletBottomSheet.kt | 90 ----------------- .../welcome/impl/ui/WelcomeSelectWallet.kt | 13 +++ 10 files changed, 263 insertions(+), 106 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt new file mode 100644 index 0000000000..d355f8ff22 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.components.bottomsheets + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Generic options bottom sheet component + * + * @param config Bottom sheet configuration containing OptionsBottomSheetContent + * @param title Title text for the bottom sheet + * @param containerColor Background color of the bottom sheet + */ +@Composable +fun OptionsBottomSheet( + config: TangemBottomSheetConfig, + title: TextReference, + containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary, +) { + TangemBottomSheet( + config = config, + titleText = title, + containerColor = containerColor, + content = { content -> + OptionsBottomSheetContent(content = content) + }, + ) +} + +@Composable +private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.options.forEachIndexed { index, option -> + InputRowDefault( + text = option.label, + showDivider = index < content.options.size - 1, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = content.options.size - 1, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(option.key) }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OptionsBottomSheetPreview() { + TangemThemePreview { + OptionsBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = "option1", + label = TextReference.Str("First Option"), + ), + BottomSheetOption( + key = "option2", + label = TextReference.Str("Second Option"), + ), + BottomSheetOption( + key = "option3", + label = TextReference.Str("Third Option"), + ), + ), + onOptionClick = {}, + ), + ), + title = TextReference.Str("Select Option"), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt new file mode 100644 index 0000000000..02d3ab9479 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.components.bottomsheets + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param key Unique identifier for the option + * @param label Display text for the option + */ +data class BottomSheetOption( + val key: String, + val label: TextReference, +) + +/** + * @param options List of options to display + * @param onOptionClick Callback when an option is clicked, receives the option key + */ +data class OptionsBottomSheetContent( + val options: ImmutableList = persistentListOf(), + val onOptionClick: (String) -> Unit = {}, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index e44ec003ea..4f3b8566e0 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) + implementation(projects.features.createWalletSelection.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..f217f837ce 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -11,4 +12,5 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index d1d008e54f..01b1ebb3b1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -6,12 +6,17 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R.* +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R -import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -20,16 +25,19 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class UserWalletListModel @Inject constructor( userWalletsFetcherFactory: UserWalletsFetcher.Factory, shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val router: Router, private val messageSender: UiMessageSender, - private val userWalletSaver: UserWalletSaver, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -45,7 +53,8 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::addUserWallet, + onAddNewWalletClick = ::showAddWalletBottomSheet, + addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -54,8 +63,9 @@ internal class UserWalletListModel @Inject constructor( flow = userWalletsFetcher.userWallets, flow2 = shouldSaveUserWalletsUseCase(), flow3 = isWalletSavingInProgress, - transform = ::updateState, - ).launchIn(modelScope) + ) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress -> + updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress) + }.launchIn(modelScope) } private fun updateState( @@ -74,7 +84,58 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun addUserWallet() = withProgress(isWalletSavingInProgress) { - userWalletSaver.scanAndSaveUserWallet(modelScope) + private fun showAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } + + private fun dismissAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false), + ) + } + } + + private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent { + return OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + dismissAddWalletBottomSheet() + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + }, + ) + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index ec27900868..bea3f8c35d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent @@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M onClick = state.onAddNewWalletClick, ) } + + AddWalletBottomSheet(state.addWalletBottomSheet) } @Composable @@ -94,6 +100,15 @@ private fun AddWalletButton( } } +@Composable +private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 8a2a3c3c3f..78977b9216 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) implementation(projects.common.routing) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index c189ac83b5..6a432d7e17 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -6,8 +6,12 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked @@ -15,11 +19,10 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.* import com.tangem.features.welcome.impl.ui.state.WelcomeUM import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -46,6 +49,8 @@ internal class WelcomeModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, private val walletsRepository: WalletsRepository, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, ) : Model() { val uiState: StateFlow @@ -149,8 +154,27 @@ internal class WelcomeModel @Inject constructor( currentState.copy( addWalletBottomSheet = TangemBottomSheetConfig( isShown = true, - content = AddWalletBottomSheetContentUM( - onOptionClick = ::onAddWalletOptionClick, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(R.string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(R.string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(R.string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + onAddWalletOptionClick(optionKey) + }, ), onDismissRequest = { updateSelectState { @@ -162,11 +186,13 @@ internal class WelcomeModel @Inject constructor( } } - private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) { - when (option) { - Create -> router.push(AppRoute.CreateWalletSelection) - Add -> router.push(AppRoute.AddExistingWallet) - Buy -> Unit // TODO + private fun onAddWalletOptionClick(optionKey: String) { + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } } @@ -246,4 +272,10 @@ internal class WelcomeModel @Inject constructor( } } } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt deleted file mode 100644 index 5d26549787..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM - -@Composable -fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - titleText = resourceReference(R.string.auth_info_add_wallet_title), - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: AddWalletBottomSheetContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - InputRowDefault( - text = resourceReference(R.string.home_button_create_new_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 0, - lastIndex = 3, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, - ) - InputRowDefault( - text = resourceReference(R.string.home_button_add_existing_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, - ) - InputRowDefault( - text = resourceReference(R.string.details_buy_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 2, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - AddWalletBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = AddWalletBottomSheetContentUM(), - ), - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index 9b3bba406d..7e9e671696 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -19,9 +19,13 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.* import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R @@ -170,4 +174,13 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.secondary, ) } +} + +@Composable +fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) } \ No newline at end of file From b6fad30af95c395aa0e68493f8b7c7a2f3b424e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 13:35:17 +0400 Subject: [PATCH 144/165] Updated on 2026-08-14 --- .../domain/account/models/AccountList.kt | 35 ++++- .../usecase/AddCryptoPortfolioUseCase.kt | 17 +-- .../usecase/RecoverCryptoPortfolioUseCase.kt | 12 +- .../usecase/UpdateCryptoPortfolioUseCase.kt | 2 +- .../domain/account/models/AccountListTest.kt | 2 +- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 10 +- .../ArchiveCryptoPortfolioUseCaseTest.kt | 6 +- .../GetUnoccupiedAccountIndexUseCaseTest.kt | 4 +- .../RecoverCryptoPortfolioUseCaseTest.kt | 12 +- .../UpdateCryptoPortfolioUseCaseTest.kt | 4 +- .../tangem/domain/account/utils/AccountExt.kt | 11 +- .../tangem/domain/models/account/Account.kt | 123 +++++++----------- .../domain/models/account/AccountTest.kt | 37 ++---- .../archived/ArchivedAccountListModel.kt | 2 +- .../createedit/AccountCreateEditModel.kt | 4 +- .../entity/AccountCreateEditUMBuilder.kt | 2 +- .../account/details/AccountDetailsModel.kt | 2 +- 17 files changed, 126 insertions(+), 159 deletions(-) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 319babf91b..89f7fd33c8 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -3,7 +3,10 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable @@ -22,6 +25,8 @@ data class AccountList private constructor( val userWallet: UserWallet, val accounts: Set, val totalAccounts: Int, + val sortType: TokensSortType, + val groupType: TokensGroupType, ) { /** Retrieves the main crypto portfolio account from the list of accounts */ @@ -48,6 +53,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -68,6 +75,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -132,6 +141,8 @@ data class AccountList private constructor( userWallet: UserWallet, accounts: Set, totalAccounts: Int, + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } @@ -149,10 +160,16 @@ data class AccountList private constructor( val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } - val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size + val uniqueAccountNameCount = accounts.map { it.accountName.value }.distinct().size ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } - AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) + AccountList( + userWallet = userWallet, + accounts = accounts, + totalAccounts = totalAccounts, + sortType = sortType, + groupType = groupType, + ) } /** @@ -160,13 +177,23 @@ data class AccountList private constructor( * * @param userWallet the user wallet associated with the account list */ - fun empty(userWallet: UserWallet): AccountList { + fun empty( + userWallet: UserWallet, + cryptoCurrencies: Set = emptySet(), + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, + ): AccountList { return AccountList( userWallet = userWallet, accounts = setOf( - Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + Account.CryptoPortfolio.createMainAccount( + userWalletId = userWallet.walletId, + cryptoCurrencies = cryptoCurrencies, + ), ), totalAccounts = 1, + sortType = sortType, + groupType = groupType, ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 75722f9824..6a3866bc4b 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -64,14 +64,9 @@ class AddCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = accountName, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } @@ -88,7 +83,13 @@ class AddCryptoPortfolioUseCase( catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - return AccountList.empty(userWallet = userWallet) + // TODO: [REDACTED_JIRA] + return AccountList.empty( + userWallet = userWallet, + cryptoCurrencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) } private suspend fun Raise.saveAccounts(accountList: AccountList) { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index 9679a036b6..f5dcec41aa 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -8,8 +8,6 @@ import arrow.core.raise.either import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId @@ -66,14 +64,10 @@ class RecoverCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = this.accountId, accountName = this.name, - accountIcon = this.icon, + icon = this.icon, derivationIndex = this.derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + // TODO: [REDACTED_JIRA] + cryptoCurrencies = emptySet(), ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index 8c2208e552..4451a5f50d 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -79,7 +79,7 @@ class UpdateCryptoPortfolioUseCase( } private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { - return if (icon != null) this.copy(accountIcon = icon) else this + return if (icon != null) this.copy(icon = icon) else this } /** diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index d5323c0a85..0c43198fa0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -123,7 +123,7 @@ class AccountListTest { accounts = setOf( Account.CryptoPortfolio.createMainAccount(userWalletId), Account.CryptoPortfolio.createMainAccount(userWalletId).copy( - accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), ), ), expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index cf47f9b807..f30bc1dfda 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -46,7 +46,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -75,7 +75,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -107,7 +107,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -138,7 +138,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -170,7 +170,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index aaea0a5379..1e442938fa 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -39,8 +39,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() @@ -130,8 +129,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! val exception = IllegalStateException("Save failed") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt index ec8af7f2b7..4c994aeb21 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -1,9 +1,9 @@ package com.tangem.domain.account.usecase import arrow.core.left -import arrow.core.right import com.google.common.truth.Truth import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import io.mockk.clearMocks import io.mockk.coEvery @@ -35,7 +35,7 @@ class GetUnoccupiedAccountIndexUseCaseTest { val actual = useCase(userWalletId = userWalletId) // Assert - val expected = 4.right() + val expected = DerivationIndex(4) Truth.assertThat(actual).isEqualTo(expected) coVerify { crudRepository.getTotalAccountsCount(userWalletId) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 316e789754..7c8f1a847f 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -43,15 +43,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() @@ -60,7 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest { val actual = useCase(account.accountId) // Assert - val expected = recoveredAccount.right() + val expected = account.right() Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { @@ -173,15 +172,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index d6638b5c05..f1ba896a7c 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -73,7 +73,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -102,7 +102,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt index 597d6aa059..9f452c4145 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -1,7 +1,5 @@ package com.tangem.domain.account.utils -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWalletId import kotlin.random.Random @@ -33,13 +31,8 @@ fun createAccount( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = AccountName(name).getOrNull()!!, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index db4a5b164c..b376be0db1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -1,9 +1,8 @@ package com.tangem.domain.models.account import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.either -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError import com.tangem.domain.models.currency.CryptoCurrency @@ -22,7 +21,7 @@ sealed interface Account { val accountId: AccountId /** Name of the account */ - val name: AccountName + val accountName: AccountName /** The identifier of the user wallet associated with the account */ val userWalletId: UserWalletId @@ -31,21 +30,19 @@ sealed interface Account { /** * Represents a crypto portfolio account * - * @property accountId unique identifier of the account - * @property name name of the account - * @property icon icon representing the account - * @property derivationIndex index used for derivation of the account - * @property isArchived indicates whether the account is archived - * @property cryptoCurrencyList list of tokens associated with the account + * @property accountId unique identifier of the account + * @property accountName name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property cryptoCurrencies set of tokens associated with the account */ @Serializable data class CryptoPortfolio private constructor( override val accountId: AccountId, - override val name: AccountName, + override val accountName: AccountName, val icon: CryptoPortfolioIcon, val derivationIndex: DerivationIndex, - val isArchived: Boolean, - val cryptoCurrencyList: CryptoCurrencyList, + val cryptoCurrencies: Set, ) : Account { /** Indicates if the account is the main account */ @@ -54,41 +51,22 @@ sealed interface Account { /** Number of tokens in the account */ val tokensCount: Int - get() = cryptoCurrencyList.currencies.size + get() = cryptoCurrencies.size /** Number of distinct networks in the account */ val networksCount: Int - get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size - fun copy( - accountName: AccountName = this.name, - accountIcon: CryptoPortfolioIcon = this.icon, - isArchived: Boolean = this.isArchived, - ): CryptoPortfolio { + fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio { return CryptoPortfolio( accountId = this.accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = this.derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = this.cryptoCurrencyList, + cryptoCurrencies = this.cryptoCurrencies, ) } - /** - * Represents a list of tokens in the account - * - * @property currencies set of cryptocurrencies in the account - * @property sortType sorting type for the tokens - * @property groupType grouping type for the tokens - */ - @Serializable - data class CryptoCurrencyList( - val currencies: Set, - val sortType: TokensSortType, - val groupType: TokensGroupType, - ) - /** * Represents possible errors when creating a crypto portfolio account */ @@ -109,33 +87,34 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param name name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param name name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ - @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, name: String, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: Int, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): Either { return either { - val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind() - val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind() + val accountName = AccountName(value = name).getOrElse { + raise(AccountNameError(cause = it)) + } + + val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + raise(DerivationIndexError(cause = it)) + } invoke( accountId = accountId, accountName = accountName, - accountIcon = accountIcon, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } } @@ -143,38 +122,39 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param accountName name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, accountName: AccountName, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): CryptoPortfolio { return CryptoPortfolio( accountId = accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } /** * Creates a main account for the given user wallet ID * - * @param userWalletId the ID of the user wallet + * @param userWalletId the ID of the user wallet + * @param cryptoCurrencies set of tokens associated with the account */ - fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + fun createMainAccount( + userWalletId: UserWalletId, + cryptoCurrencies: Set = emptySet(), + ): CryptoPortfolio { val derivationIndex = DerivationIndex.Main return CryptoPortfolio( @@ -182,15 +162,10 @@ sealed interface Account { userWalletId = userWalletId, derivationIndex = derivationIndex, ), - name = AccountName.Main, + accountName = AccountName.Main, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = cryptoCurrencies, ) } } diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index eabb287672..91baaf2a76 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -1,10 +1,7 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio -import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -100,13 +97,12 @@ class AccountTest { val name = "" // Act - val actual = CryptoPortfolio( + val actual = CryptoPortfolio.invoke( accountId = mockk(), name = name, - accountIcon = mockk(), + icon = mockk(), derivationIndex = 0, - isArchived = false, - cryptoCurrencyList = mockk(), + cryptoCurrencies = emptySet(), ) .leftOrNull()!! @@ -125,14 +121,9 @@ class AccountTest { derivationIndex = derivationIndex, ), name = "Test Account", - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), derivationIndex = derivationIndex.value, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) .getOrNull()!! @@ -157,14 +148,9 @@ class AccountTest { derivationIndex = derivationIndex, ), accountName = AccountName.Main, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) Truth.assertThat(actual).isEqualTo(expected) @@ -182,14 +168,9 @@ class AccountTest { return CryptoPortfolio.invoke( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex), name = name, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = currencies, - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = currencies, ) .getOrNull()!! } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index 3c2aed7f6b..a93cdcca2b 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -48,7 +48,7 @@ internal class ArchivedAccountListModel @Inject constructor( ) messageSender.send( DialogMessage( - title = stringReference(account.name.value), + title = stringReference(account.accountName.value), message = TextReference.EMPTY, firstActionBuilder = { firstAction }, secondActionBuilder = { secondAction }, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index af1c7d4ed1..5c61ebc3f4 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -109,7 +109,7 @@ internal class AccountCreateEditModel @Inject constructor( val state = uiState.value val name = AccountName(state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() - val isNewName = name != params.account.name + val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon updateCryptoPortfolioUseCase( icon = if (isNewIcon) icon else null, @@ -143,7 +143,7 @@ internal class AccountCreateEditModel @Inject constructor( val isAvailableForConfirm = when (params) { is AccountCreateEditComponent.Params.Create -> isValidName is AccountCreateEditComponent.Params.Edit -> { - val isNewName = this.account.name != params.account.name.value + val isNewName = this.account.name != params.account.accountName.value val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon isValidName && (isNewName || isNewIcon) } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 654515e302..24b69681c9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -34,7 +34,7 @@ internal class AccountCreateEditUMBuilder( onNameChange = onNameChange, ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( - name = params.account.name.value, + name = params.account.accountName.value, portfolioIcon = params.account.portfolioIcon.toUM(), derivationInfo = createAccountDerivationInfo( index = (params.account as Account.CryptoPortfolio).derivationIndex.value, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index d6fc77713e..0e40b6b39d 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -74,7 +74,7 @@ internal class AccountDetailsModel @Inject constructor( private fun getInitialState(): AccountDetailsUM { return AccountDetailsUM( - accountName = params.account.name.value, + accountName = params.account.accountName.value, accountIcon = params.account.portfolioIcon.toUM(), onCloseClick = { router.pop() }, onAccountEditClick = ::onEditAccountClick, From 30a7d0dcecd4cf29220e16e25b966f851562e8d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 18:27:08 +0400 Subject: [PATCH 145/165] Updated on 2026-08-14 --- .../CachedCurrenciesStatusesOperations.kt | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index c82e831875..70700beea8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -168,15 +168,18 @@ class CachedCurrenciesStatusesOperations( combine( flow = getQuotes(currenciesIds), flow2 = networksStatusesUpdates, - flow3 = networksStatusesUpdates.flatMapLatest { - val currenciesAddresses = it.getOrElse(default = { emptySet() }) - .mapNotNull { - val currency = currencies.firstOrNull { currency -> currency.network == it.network } - ?: return@mapNotNull null + flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses -> + val networksStatuses = maybeNetworksStatuses.getOrNull() - currency.id to extractAddress(it) + val currenciesAddresses = if (networksStatuses == null) { + emptyMap() + } else { + currencies.associate { currency -> + val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } + + currency.id to extractAddress(networkStatus) } - .toMap() + } getYieldsBalancesUpdates(userWalletId, currenciesAddresses) }, @@ -397,8 +400,11 @@ class CachedCurrenciesStatusesOperations( return channelFlow { val state = MutableStateFlow(emptyList()) - val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(currencyId = it.key, defaultAddress = it.value) + val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> + stakingIdFactory.create( + currencyId = currencyWithAddress.key, + defaultAddress = currencyWithAddress.value, + ) .getOrNull() } From 009f636520c0f52dfaa77dfc78e1348d94114670 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:50:30 +0300 Subject: [PATCH 146/165] Updated on 2026-08-14 --- .../main/assets/configs/feature_toggles_config.json | 10 +++++----- gradle/tangem_dependencies.toml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0d8eea380b..446b607df1 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -9,7 +9,7 @@ }, { "name": "STAKING_TON_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "NFT_MEDIA_CONTENT_ENABLED", @@ -17,7 +17,7 @@ }, { "name": "STAKING_CARDANO_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_CONNECT_REDESIGN_ENABLED", @@ -33,15 +33,15 @@ }, { "name": "SEND_VIA_SWAP_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "SWAP_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "SEND_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_BALANCE_FETCHER_ENABLED", diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f430a4dd17..418aa40d43 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1205" +tangemBlockchainSdk = "releases-5.28-1206" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-557" +tangemCardSdk = "releases-5.28-559" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 4b719be68338460597b3ae3ed8d6539db9bd86d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:54:25 +0300 Subject: [PATCH 147/165] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 446b607df1..6c01d5db1e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -37,7 +37,7 @@ }, { "name": "SWAP_REDESIGN_ENABLED", - "version": "5.28.0" + "version": "undefined" }, { "name": "SEND_REDESIGN_ENABLED", From 088a3471d953c0d258a950ed67cf8a8e79580dca Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:55:47 +0300 Subject: [PATCH 148/165] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6c01d5db1e..4fa13e88c4 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -53,7 +53,7 @@ }, { "name": "NFT_SEND_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "TANGEM_PAY_ENABLED", From 0c5d018677198aca94e2df901169a6bb4c1d71d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 19:56:51 +0500 Subject: [PATCH 149/165] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 22 ++++++++++- .../components/notifications/Notification.kt | 4 +- .../notifications/NotificationConfig.kt | 3 ++ .../res/drawable/ic_hardware_backup_36.xml | 22 +++++++++++ .../data/wallets/DefaultWalletsRepository.kt | 14 +++++++ .../wallets/repository/WalletsRepository.kt | 4 ++ ...DismissUpgradeWalletNotificationUseCase.kt | 12 ++++++ ...UpgradeWalletNotificationEnabledUseCase.kt | 13 +++++++ .../preview/PreviewWalletSettingsComponent.kt | 3 ++ .../entity/WalletSettingsItemUM.kt | 8 ++++ .../walletsettings/entity/WalletSettingsUM.kt | 1 + .../model/WalletSettingsModel.kt | 23 +++++++++++- .../walletsettings/ui/WalletSettingsScreen.kt | 37 +++++++++++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 33 +++++++++++++++++ .../wallet/state/model/WalletNotification.kt | 2 + .../components/common/WalletNotifications.kt | 6 --- 16 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_hardware_backup_36.xml create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 44dd702fc3..911eededb2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -25,7 +25,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LargeClass") @Module @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @@ -352,4 +352,24 @@ internal object WalletsDomainModule { walletsRepository = walletsRepository, ) } + + @Provides + @Singleton + fun providesIsUpgradeWalletNotificationEnabledUseCase( + walletsRepository: WalletsRepository, + ): IsUpgradeWalletNotificationEnabledUseCase { + return IsUpgradeWalletNotificationEnabledUseCase( + walletsRepository = walletsRepository, + ) + } + + @Provides + @Singleton + fun providesDismissUpgradeWalletNotificationUseCase( + walletsRepository: WalletsRepository, + ): DismissUpgradeWalletNotificationUseCase { + return DismissUpgradeWalletNotificationUseCase( + walletsRepository = walletsRepository, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 7b1baa0109..06a2bc1d9e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -27,7 +27,6 @@ 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.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize @@ -64,7 +63,6 @@ fun Notification( NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention }, - iconSize: Dp = 20.dp, isEnabled: Boolean = true, ) { NotificationBaseContainer( @@ -78,7 +76,7 @@ fun Notification( MainContent( iconResId = config.iconResId, iconTint = iconTint, - iconSize = iconSize, + iconSize = config.iconSize, title = config.title, titleColor = titleColor, subtitle = config.subtitle, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 4afe96144e..ca5cdc8e5a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -1,6 +1,8 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.TextReference /** @@ -26,6 +28,7 @@ data class NotificationConfig( val onCloseClick: (() -> Unit)? = null, val showArrowIcon: Boolean = onClick != null, val iconTint: IconTint = IconTint.Unspecified, + val iconSize: Dp = 20.dp, ) { sealed class ButtonsState { diff --git a/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml new file mode 100644 index 0000000000..071683ab08 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_hardware_backup_36.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 2127d9b80b..d2a4f5cea1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -31,6 +31,7 @@ import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.collections.mutableSetOf typealias SeedPhraseNotificationsStatuses = Map @@ -44,6 +45,9 @@ internal class DefaultWalletsRepository( private val authProvider: AuthProvider, ) : WalletsRepository { + private val upgradeWalletNotificationDisabled: MutableStateFlow> = + MutableStateFlow(mutableSetOf()) + override suspend fun shouldSaveUserWalletsSync(): Boolean { return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } @@ -327,6 +331,16 @@ internal class DefaultWalletsRepository( } } + override fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow { + return upgradeWalletNotificationDisabled.map { + it.contains(userWalletId) + } + } + + override suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) { + upgradeWalletNotificationDisabled.update { it.plus(userWalletId) } + } + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { tangemTechApi.updateWallet( walletId = walletId, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index a1eb1f0cd8..b65e7006cc 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -57,6 +57,10 @@ interface WalletsRepository { suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) + fun isUpgradeWalletNotificationEnabled(userWalletId: UserWalletId): Flow + + suspend fun dismissUpgradeWalletNotification(userWalletId: UserWalletId) + @Throws suspend fun setWalletName(walletId: String, walletName: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt new file mode 100644 index 0000000000..2aeb755fba --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DismissUpgradeWalletNotificationUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class DismissUpgradeWalletNotificationUseCase( + private val walletsRepository: WalletsRepository, +) { + suspend operator fun invoke(userWalletId: UserWalletId) { + walletsRepository.dismissUpgradeWalletNotification(userWalletId) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt new file mode 100644 index 0000000000..d38fec4fe2 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsUpgradeWalletNotificationEnabledUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.flow.Flow + +class IsUpgradeWalletNotificationEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return walletsRepository.isUpgradeWalletNotificationEnabled(userWalletId) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 453120249f..1ac9106632 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -45,6 +45,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onNotificationsDescriptionClick = {}, isNotificationsPermissionGranted = false, onAccessCodeClick = {}, + walletUpgradeDismissed = false, + onUpgradeWalletClick = {}, + onDismissUpgradeWalletClick = {}, ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 490fb2b00e..91a77aec05 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -43,4 +43,12 @@ internal sealed class WalletSettingsItemUM { val title: TextReference, val description: TextReference, ) : WalletSettingsItemUM() + + data class UpgradeWallet( + override val id: String, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + val onDismissClick: () -> Unit, + ) : WalletSettingsItemUM() } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 39bd16ae56..04014e4662 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -10,4 +10,5 @@ internal data class WalletSettingsUM( val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, val isWalletBackedUp: Boolean = true, + val walletUpgradeDismissed: Boolean = false, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index f9d0b633b2..c190745304 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -56,7 +56,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class WalletSettingsModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, @@ -80,6 +80,8 @@ internal class WalletSettingsModel @Inject constructor( private val permissionsRepository: PermissionRepository, private val notificationsRepository: NotificationsRepository, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, + private val isUpgradeWalletNotificationEnabledUseCase: IsUpgradeWalletNotificationEnabledUseCase, + private val dismissUpgradeWalletNotificationUseCase: DismissUpgradeWalletNotificationUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -93,6 +95,7 @@ internal class WalletSettingsModel @Inject constructor( requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, isWalletBackedUp = true, + walletUpgradeDismissed = false, ), ) @@ -120,7 +123,8 @@ internal class WalletSettingsModel @Inject constructor( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), getWalletNFTEnabledUseCase.invoke(params.userWalletId), getWalletNotificationsEnabledUseCase(params.userWalletId), - ) { maybeWallet, nftEnabled, notificationsEnabled -> + isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), + ) { maybeWallet, nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() val isWalletBackedUp = when (wallet) { @@ -139,6 +143,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), + isUpgradeNotificationEnabled = isUpgradeNotificationEnabled, ), isWalletBackedUp = isWalletBackedUp, ) @@ -165,6 +170,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, isNotificationsPermissionGranted: Boolean, + isUpgradeNotificationEnabled: Boolean, ): PersistentList { val isMultiCurrency = when (userWallet) { is UserWallet.Cold -> userWallet.isMultiCurrency @@ -215,6 +221,9 @@ internal class WalletSettingsModel @Inject constructor( onCheckedNotificationsChanged = ::onCheckedNotificationsChange, onNotificationsDescriptionClick = ::onNotificationsDescriptionClick, onAccessCodeClick = ::onAccessCodeClick, + walletUpgradeDismissed = isUpgradeNotificationEnabled, + onUpgradeWalletClick = ::onUpgradeWalletClick, + onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, ) } @@ -367,4 +376,14 @@ internal class WalletSettingsModel @Inject constructor( router.push(AppRoute.UpdateAccessCode(params.userWalletId)) } } + + private fun onUpgradeWalletClick() { + // TODO [REDACTED_TASK_KEY] + } + + private fun onDismissUpgradeWalletClick() { + modelScope.launch { + dismissUpgradeWalletNotificationUseCase.invoke(params.userWalletId) + } + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index fe95f8129a..8c4bbd6cf2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -116,6 +117,10 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) + is WalletSettingsItemUM.UpgradeWallet -> UpgradeWalletBlock( + modifier = itemModifier, + model = item, + ) } } } @@ -225,6 +230,21 @@ private fun SwitchBlock(model: WalletSettingsItemUM.WithSwitch, modifier: Modifi } } +@Composable +private fun UpgradeWalletBlock(model: WalletSettingsItemUM.UpgradeWallet, modifier: Modifier = Modifier) { + Notification( + config = NotificationConfig( + title = model.title, + subtitle = model.description, + iconResId = R.drawable.ic_hardware_backup_36, + iconSize = 36.dp, + onClick = model.onClick, + onCloseClick = model.onDismissClick, + ), + modifier = modifier, + ) +} + @Composable private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermission, modifier: Modifier = Modifier) { Notification( @@ -266,4 +286,21 @@ private fun Preview_WalletSettingsScreen() { PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) } } + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WalletSettingsScreen1() { + TangemThemePreview { + UpgradeWalletBlock( + model = WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = stringReference("Upgrade wallet with a hardware backup"), + description = stringReference("Keep your crypto safe with Tangem’s best-in-class hardware wallet."), + onClick = {}, + onDismissClick = {}, + ), + ) + } +} // endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 5086f22579..4189d78f2a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -47,8 +47,19 @@ internal class ItemsBuilder @Inject constructor( onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, onAccessCodeClick: () -> Unit, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, ): PersistentList = persistentListOf() .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) + .addAll( + buildUpgradeWalletItem( + userWallet = userWallet, + walletUpgradeDismissed = walletUpgradeDismissed, + onUpgradeWalletClick = onUpgradeWalletClick, + onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, + ), + ) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( @@ -124,6 +135,28 @@ internal class ItemsBuilder @Inject constructor( onCheckedChange = onCheckedNFTChange, ) + private fun buildUpgradeWalletItem( + userWallet: UserWallet, + walletUpgradeDismissed: Boolean, + onUpgradeWalletClick: () -> Unit, + onDismissUpgradeWalletClick: () -> Unit, + ): List = when (userWallet) { + is UserWallet.Cold -> emptyList() + is UserWallet.Hot -> if (!walletUpgradeDismissed) { + listOf( + WalletSettingsItemUM.UpgradeWallet( + id = "upgrade_wallet", + title = resourceReference(id = R.string.hw_upgrade_to_cold_banner_title), + description = resourceReference(id = R.string.hw_upgrade_to_cold_banner_description), + onClick = onUpgradeWalletClick, + onDismissClick = onDismissUpgradeWalletClick, + ), + ) + } else { + emptyList() + } + } + private fun buildNotificationsPermissionItem() = WalletSettingsItemUM.NotificationPermission( id = "notifications_permission", title = resourceReference(id = R.string.transaction_notifications_warning_title), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 965b55df30..3d23c93ae5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference @@ -282,6 +283,7 @@ sealed class WalletNotification(val config: NotificationConfig) { text = resourceReference(R.string.notification_referral_promo_button), onClick = onClick, ), + iconSize = 54.dp, ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 0227a5339a..211ce57e2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme @@ -39,11 +38,6 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning is WalletNotification.Informational -> TangemTheme.colors.icon.accent From b8f3d5315ff55f1ceb2c0e0c6918e98d014d696c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:11:54 +0200 Subject: [PATCH 150/165] Updated on 2026-08-14 --- .../impl/DefaultMarketsPortfolioComponent.kt | 29 +++++ .../impl/model/MarketsPortfolioModel.kt | 74 ++++++++++- .../nft/common/DefaultNFTComponent.kt | 3 + .../nft/receive/NFTReceiveComponent.kt | 29 +++++ .../nft/receive/model/NFTReceiveModel.kt | 95 +++++++++++++-- .../impl/DefaultOnboardingNoteComponent.kt | 3 + .../topup/OnboardingNoteTopUpComponent.kt | 29 +++++ .../topup/model/OnboardingNoteTopUpModel.kt | 70 ++++++++++- .../impl/DefaultOnboardingTwinComponent.kt | 29 +++++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 115 ++++++++++++++---- .../component/DefaultTokenReceiveComponent.kt | 69 +---------- .../model/TokenReceiveQrCodeModel.kt | 4 +- .../tokenreceive/ui/TokenReceiveContent.kt | 65 ++++++++++ .../tokenreceive/ui/state/QrCodeUM.kt | 2 +- .../wallet/child/wallet/WalletComponent.kt | 12 ++ .../WalletCurrencyActionsClickIntents.kt | 72 +++++++++-- .../router/DefaultWalletRouter.kt | 8 ++ .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/WalletStateController.kt | 10 ++ .../wallet/state/model/WalletDialogConfig.kt | 4 + 20 files changed, 607 insertions(+), 118 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 73841ec05d..8f3e1d6da7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -5,12 +5,20 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -19,20 +27,41 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: MarketsPortfolioComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : AppComponentContext by context, MarketsPortfolioComponent { private val model: MarketsPortfolioModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() MyPortfolio(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : MarketsPortfolioComponent.Factory { override fun create( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index e17c925595..60673724c0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -15,24 +17,31 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.ArtworkModel +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,7 +52,7 @@ import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class MarketsPortfolioModel @Inject constructor( @@ -60,6 +69,9 @@ internal class MarketsPortfolioModel @Inject constructor( private val getCardImageUseCase: GetCardImageUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { val state: StateFlow get() = _state @@ -80,6 +92,8 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } @@ -128,7 +142,11 @@ internal class MarketsPortfolioModel @Inject constructor( tokenActionsHandler = tokenActionsIntentsFactory.create( currentAppCurrency = Provider { currentAppCurrency.value }, updateTokenReceiveBSConfig = { updateBlock -> - updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) { + updateTokensState { + it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) + } + } }, onHandleQuickAction = { handledAction -> analyticsEventHandler.send( @@ -137,6 +155,9 @@ internal class MarketsPortfolioModel @Inject constructor( blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, ), ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + configureReceiveAddresses(handledAction) + } }, ), updateTokens = { updateBlock -> @@ -359,4 +380,51 @@ internal class MarketsPortfolioModel @Inject constructor( block(tokensState) } } + + private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { + when (quickAction.action) { + TokenActionsBSContentUM.Action.Receive -> { + val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return + val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency + modelScope.launch { + val ensName = getEnsNameUseCase.invoke( + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + val tokenConfig = TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + bottomSheetNavigation.activate(tokenConfig) + } + } + else -> Unit + } + } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 5c4874dd5e..6aa2bc4a8f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -22,6 +22,7 @@ import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.entity.NFTSendSuccessListener import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.traits.NFTAssetTraitsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,6 +36,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( @Assisted private val params: NFTComponent.Params, private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, nftSendSuccessListener: NFTSendSuccessListener, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( walletName = params.walletName, onBackClick = ::onChildBack, ), + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) private fun getDetailsComponent( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index f445c95fb7..e4bd9a112e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -4,29 +4,58 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.receive.ui.NFTReceive +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedInject internal class NFTReceiveComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by context { private val model: NFTReceiveModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NFTReceive(state, modifier) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class Params( val userWalletId: UserWalletId, val walletName: String, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index fc2df6f5c2..632959d7e8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.nft.receive.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -12,12 +14,20 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.TokenReceiveNotification import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTCurrencyUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.nft.GetNFTNetworksUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.receive.entity.NFTReceiveUM @@ -25,6 +35,8 @@ import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheet import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* @@ -43,11 +55,17 @@ internal class NFTReceiveModel @Inject constructor( private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, private val messageSender: UiMessageSender, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, + private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase, paramsContainer: ParamsContainer, ) : Model() { private val params: NFTReceiveComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _state = MutableStateFlow( value = NFTReceiveUM( onBackClick = params.onBackClick, @@ -138,14 +156,23 @@ internal class NFTReceiveModel @Inject constructor( when (val value = networkStatus.value) { is NetworkStatus.Verified -> { - _state.update { - ShowReceiveBottomSheetTransformer( - network = network, - networkAddress = value.address, - onDismissBottomSheet = ::onReceiveBottomSheetDismiss, - onCopyClick = { text -> onCopyClick(text, network) }, - onShareClick = { text -> onShareClick(text, network) }, - ).transform(it) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses( + addresses = value.address, + network = network, + ), + ) + } else { + _state.update { + ShowReceiveBottomSheetTransformer( + network = network, + networkAddress = value.address, + onDismissBottomSheet = ::onReceiveBottomSheetDismiss, + onCopyClick = { text -> onCopyClick(text, network) }, + onShareClick = { text -> onShareClick(text, network) }, + ).transform(it) + } } } is NetworkStatus.MissedDerivation, @@ -166,4 +193,56 @@ internal class NFTReceiveModel @Inject constructor( analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name)) shareManager.shareText(text = text) } + + private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig { + val cryptoCurrency = getNFTCurrencyUseCase.invoke(network) + + val ensName = getEnsNameUseCase.invoke( + userWalletId = params.userWalletId, + network = network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = cryptoCurrency.symbol, + ), + ) + } + } + + val notifications = buildList { + if (BlockchainUtils.isSolana(network.rawId)) { + add( + TokenReceiveNotification( + title = R.string.nft_receive_unsupported_types, + subtitle = R.string.nft_receive_unsupported_types_description, + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = params.userWalletId, + showMemoDisclaimer = false, + receiveAddress = receiveAddresses, + tokenReceiveNotification = notifications, + asset = Asset.NFT, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index bea41ef278..eab88630a0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -28,6 +28,7 @@ import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -108,6 +110,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onDone = { params.onDone() }, ), + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt index 5b9cc4f1af..771e29c209 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt @@ -6,23 +6,40 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp +import com.tangem.features.tokenreceive.TokenReceiveComponent internal class OnboardingNoteTopUpComponent( appComponentContext: AppComponentContext, private val params: Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) @@ -30,8 +47,20 @@ internal class OnboardingNoteTopUpComponent( modifier = modifier, state = state, ) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class Params( val childParams: DefaultOnboardingNoteComponent.ChildParams, val onDone: () -> Unit, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 4f0a65a810..a8e760dbf3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped @@ -13,23 +15,29 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isPositive import kotlinx.coroutines.flow.* @@ -54,6 +62,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { private val params = paramsContainer.require() @@ -61,6 +72,8 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val scanResponse = params.childParams.commonState.value.scanResponse private var userWallet = params.childParams.commonState.value.userWallet + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _uiState = MutableStateFlow( OnboardingNoteTopUpUM( onRefreshBalanceClick = ::refreshBalance, @@ -112,8 +125,18 @@ internal class OnboardingNoteTopUpModel @Inject constructor( val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return val networkAddress = currencyStatus.value.networkAddress ?: return - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + val userWalletId = userWallet?.walletId ?: return + modelScope.launch { + configureReceiveAddresses( + cryptoCurrencyStatus = currencyStatus, + userWalletId = userWalletId, + )?.let { bottomSheetNavigation.activate(it) } + } + } else { + _uiState.update { + it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + } } Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) } @@ -250,4 +273,47 @@ internal class OnboardingNoteTopUpModel @Inject constructor( saveWalletUseCase(wallet, false) return wallet } + + private suspend fun configureReceiveAddresses( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, + ): TokenReceiveConfig? { + val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index ded3238c83..0c4797f1a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -5,16 +5,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder import com.tangem.core.decompose.navigation.inner.InnerNavigationState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -23,10 +31,18 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnboardingTwinComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingTwinComponent, AppComponentContext by appComponentContext, InnerNavigationHolder { private val model: OnboardingTwinModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + init { params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) } @@ -43,14 +59,27 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { BackHandler { model.onBack() } + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() OnboardingTwin( state = state, modifier = modifier, ) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class TwinInnerNavigationState( override val stackSize: Int, ) : InnerNavigationState { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 2e3a94788a..f0076b3bee 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -30,6 +32,8 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse @@ -38,9 +42,11 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -52,6 +58,7 @@ import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -89,12 +96,17 @@ internal class OnboardingTwinModel @Inject constructor( private val shareManager: ShareManager, private val tokensFeatureToggles: TokensFeatureToggles, private val walletBalanceFetcher: WalletBalanceFetcher, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { private val params = paramsContainer.require() private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") private val cryptoCurrencyStatusJobHolder = JobHolder() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _uiState = MutableStateFlow( when (params.mode) { Mode.WelcomeOnly -> { @@ -400,35 +412,43 @@ internal class OnboardingTwinModel @Inject constructor( val currency = status.currency val networkAddress = status.value.networkAddress ?: return - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + modelScope.launch { + configureReceiveAddresses(cryptoCurrencyStatus = status)?.let { + bottomSheetNavigation.activate(it) + } + } + } else { + update { + it.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + update { + it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) + } + }, + content = TokenReceiveBottomSheetConfig( + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, + showMemoDisclaimer = + currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + shareManager.shareText(text = it) + }, ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, ), - ), - ) + ) + } } } @@ -506,4 +526,45 @@ internal class OnboardingTwinModel @Inject constructor( TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard } + + private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { + val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: return null + val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWallet.walletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index 291ad7b029..e467be69ac 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -2,28 +2,19 @@ package com.tangem.features.tokenreceive.component import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.tokenreceive.model.TokenReceiveModel import com.tangem.features.tokenreceive.route.TokenReceiveRoutes -import com.tangem.features.tokenreceive.ui.TokenReceiveContent +import com.tangem.features.tokenreceive.ui.TokenReceiveContentSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -133,64 +124,6 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( } } -@Composable -internal fun TokenReceiveContentSheet( - route: TokenReceiveRoutes, - onCloseClick: () -> Unit, - onBackClick: () -> Unit, - contentStack: ChildStack, -) { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onCloseClick, - content = TangemBottomSheetConfigContent.Empty, - ), - onBack = onBackClick, - containerColor = TangemTheme.colors.background.tertiary, - title = { - Title( - route = route, - onBackClick = onBackClick, - onCloseClick = onCloseClick, - ) - }, - content = { - TokenReceiveContent( - stackState = contentStack, - modifier = Modifier, - ) - }, - ) -} - -@Composable -private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { - when (route) { - is TokenReceiveRoutes.QrCode -> { - TangemModalBottomSheetTitle( - startIconRes = R.drawable.ic_back_24, - onStartClick = onBackClick, - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - TokenReceiveRoutes.ReceiveAssets -> { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.domain_receive_assets_navigation_title), - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - TokenReceiveRoutes.Warning -> { - TangemModalBottomSheetTitle( - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - } -} - internal interface TokenReceiveModelCallback : TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback, TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback, diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt index e7b1915b1b..68246b663f 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent import com.tangem.features.tokenreceive.entity.ReceiveAddress import com.tangem.features.tokenreceive.ui.state.QrCodeUM @@ -27,7 +27,7 @@ internal class TokenReceiveQrCodeModel @Inject constructor( QrCodeUM( network = params.network, addressValue = params.address.value, - addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: stringReference(""), + addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY, onCopyClick = { params.callback.onCopyClick(params.id) }, onShareClick = params.callback::onShareClick, ), diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt index 5571b27657..75ee1fb93f 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt @@ -8,9 +8,74 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.fade import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +@Composable +internal fun TokenReceiveContentSheet( + route: TokenReceiveRoutes, + onCloseClick: () -> Unit, + onBackClick: () -> Unit, + contentStack: ChildStack, +) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBackClick, + containerColor = TangemTheme.colors.background.tertiary, + title = { + Title( + route = route, + onBackClick = onBackClick, + onCloseClick = onCloseClick, + ) + }, + content = { + TokenReceiveContent( + stackState = contentStack, + modifier = Modifier, + ) + }, + ) +} + +@Composable +private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { + when (route) { + is TokenReceiveRoutes.QrCode -> { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.ReceiveAssets -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.domain_receive_assets_navigation_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.Warning -> { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + } +} + @Composable internal fun TokenReceiveContent( stackState: ChildStack, diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt index 274735da2a..f9425a4fc6 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.tokenreceive.ui.state import com.tangem.core.ui.extensions.TextReference internal data class QrCodeUM( - val onCopyClick: (Int) -> Unit, + val onCopyClick: () -> Unit, val onShareClick: (String) -> Unit, val addressName: TextReference, val addressValue: String, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 07a629a880..94c51ca420 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -23,11 +23,13 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, @@ -35,6 +37,7 @@ internal class WalletComponent @AssistedInject constructor( private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -76,6 +79,15 @@ internal class WalletComponent @AssistedInject constructor( modelCallbacks = model.askForPushNotificationsModelCallbacks, ), ) + is WalletDialogConfig.TokenReceive -> { + tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = dialogConfig.tokenReceiveConfig, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 29c08244a6..71d5e76c5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -29,6 +29,8 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -41,6 +43,7 @@ import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -49,6 +52,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -61,6 +65,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -128,6 +133,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val shareManager: ShareManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -172,13 +180,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event?.let { analyticsEventHandler.send(it) } - stateHolder.showBottomSheet( - createReceiveBottomSheetContent( - currency = cryptoCurrencyStatus.currency, - addresses = cryptoCurrencyStatus.value.networkAddress ?: return, - ), - userWalletId, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + stateHolder.hideBottomSheet() + modelScope.launch { + configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let { + router.openTokenReceiveBottomSheet(it) + } + } + } else { + stateHolder.showBottomSheet( + createReceiveBottomSheetContent( + currency = cryptoCurrencyStatus.currency, + addresses = cryptoCurrencyStatus.value.networkAddress ?: return, + ), + userWalletId, + ) + } } override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? { @@ -639,4 +656,45 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( targetRoute } } + + private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null + val userWalletId = stateHolder.getSelectedWalletId() + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + address = networkAddress.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + networkAddress.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 8c39cfc9e5..602de14421 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,11 +1,13 @@ package com.tangem.feature.wallet.presentation.router import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -98,4 +100,10 @@ internal class DefaultWalletRouter @Inject constructor( ), ) } + + override fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) { + dialogNavigation.activate( + configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index b1670c0110..946a3e7692 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -54,4 +55,6 @@ internal interface InnerWalletRouter { /** Open NFT collections screen */ fun openNFT(userWallet: UserWallet) + + fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 1cb651abd1..de8c2ee08a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -90,6 +90,16 @@ internal class WalletStateController @Inject constructor() { ) } + fun hideBottomSheet() { + update( + transformer = OpenBottomSheetTransformer( + userWalletId = getSelectedWalletId(), + content = TangemBottomSheetConfigContent.Empty, + onDismissBottomSheet = {}, + ), + ) + } + private fun getInitialState(): WalletScreenState { return WalletScreenState( topBarConfig = WalletTopBarConfig(onDetailsClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index 2134c40ef5..9bce963c5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.model +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -19,4 +20,7 @@ internal sealed interface WalletDialogConfig { @Serializable data object AskForPushNotifications : WalletDialogConfig + + @Serializable + data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig } \ No newline at end of file From 7de9a905988c32aa0f983220da08b888921dfc78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 01:32:14 +0500 Subject: [PATCH 151/165] Updated on 2026-08-14 --- features/details/impl/build.gradle.kts | 1 + .../details/model/UserWalletListModel.kt | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 4f3b8566e0..8f41e0f6c1 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) implementation(projects.features.createWalletSelection.api) + implementation(projects.features.hotWallet.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 01b1ebb3b1..bfdc9b284b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R +import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -38,6 +40,8 @@ internal class UserWalletListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val userWalletSaver: UserWalletSaver, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -53,7 +57,7 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::showAddWalletBottomSheet, + onAddNewWalletClick = ::onAddNewWalletClick, addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -76,7 +80,7 @@ internal class UserWalletListModel @Inject constructor( value.copy( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = if (shouldSaveUserWallets) { + addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) { resourceReference(R.string.user_wallet_list_add_button) } else { resourceReference(R.string.scan_card_settings_button) @@ -84,15 +88,21 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun showAddWalletBottomSheet() { - state.update { currentState -> - currentState.copy( - addWalletBottomSheet = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = ::dismissAddWalletBottomSheet, - content = createAddWalletBottomSheetContent(), - ), - ) + private fun onAddNewWalletClick() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } else { + withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet(modelScope) + } } } From c372e4faee3a2b5e5577b0fbbb169a02b09d822d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 12:47:58 +0500 Subject: [PATCH 152/165] Updated on 2026-08-14 --- .../send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index d834d13885..ce5e090b22 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -349,6 +349,10 @@ internal class NFTSendConfirmModel @Inject constructor( updateTransactionStatus(txData) sendBalanceUpdater.scheduleUpdates() nftSendAnalyticHelper.nftSendSuccessAnalytics(cryptoCurrency, uiState.value) + if (uiState.value.isRedesignEnabled) { + params.callback.onResult(uiState.value) + params.onSendTransaction() + } }, ) } From 96baa581058df5940ac84daafbcace7a7fa5222d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 12:48:30 +0500 Subject: [PATCH 153/165] Updated on 2026-08-14 --- .../confirm/model/NFTSendConfirmModel.kt | 25 +- ...endConfirmationNotificationsTransformer.kt | 4 +- ...dConfirmationNotificationsTransformerV2.kt | 92 ++ ...NotificationsTransformersComparisonTest.kt | 822 ++++++++++++++++++ ...onfirmationNotificationsTransformerTest.kt | 381 ++++++++ ...firmationNotificationsTransformerV2Test.kt | 450 ++++++++++ ...otificationsTransformersComparisonTest.kt} | 2 +- 7 files changed, 1767 insertions(+), 9 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt create mode 100644 features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt create mode 100644 features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt create mode 100644 features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt rename features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/{TransformersComparisonTest.kt => SendConfirmNotificationsTransformersComparisonTest.kt} (99%) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index ce5e090b22..45ace3d940 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -47,6 +47,7 @@ import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmInitialStateTransformer import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmSendingStateTransformer import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger @@ -393,13 +394,23 @@ internal class NFTSendConfirmModel @Inject constructor( } _uiState.update { it.copy( - confirmUM = NFTSendConfirmationNotificationsTransformer( - feeUM = uiState.value.feeUM, - analyticsEventHandler = analyticsEventHandler, - cryptoCurrency = cryptoCurrencyStatus.currency, - analyticsCategoryName = analyticsCategoryName, - appCurrency = params.appCurrency, - ).transform(uiState.value.confirmUM), + confirmUM = if (uiState.value.isRedesignEnabled) { + NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = uiState.value.feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + appCurrency = params.appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + } else { + NFTSendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + analyticsCategoryName = analyticsCategoryName, + appCurrency = params.appCurrency, + ) + }.transform(uiState.value.confirmUM), ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt index 9232bc21da..b5bd158810 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformer.kt @@ -62,8 +62,10 @@ internal class NFTSendConfirmationNotificationsTransformer( val feeUM = feeUM as? FeeUM.Content val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee ?: return TextReference.EMPTY + val fiatFeeValue = feeUM.rate?.let { fee.amount.value?.multiply(it) } + val fiatFee = formatFooterFiatFee( - amount = fee.amount, + amount = fee.amount.copy(value = fiatFeeValue), isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, isFeeApproximate = feeUM.isFeeApproximate, appCurrency = appCurrency, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt new file mode 100644 index 0000000000..d69431a549 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.sendnft.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils +import com.tangem.features.send.v2.api.utils.formatFooterFiatFee +import com.tangem.features.send.v2.api.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class NFTSendConfirmationNotificationsTransformerV2( + private val feeSelectorUM: FeeSelectorUM, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, + private val appCurrency: AppCurrency, + private val analyticsCategoryName: String, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content ?: return prevState + return state.copy( + sendingFooter = getSendingFooterText(), + notifications = buildList { + addTooHighNotification(feeSelectorUM) + addTooLowNotification(feeSelectorUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { + if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + CommonSendAnalyticEvents.NoticeTransactionDelays( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) { + val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } + + private fun getSendingFooterText(): TextReference { + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content + val fee = feeSelectorUM?.selectedFeeItem?.fee ?: return TextReference.EMPTY + + val fiatFeeValue = feeSelectorUM.feeFiatRateUM?.rate?.let { fee.amount.value?.multiply(it) } + + val fiatFee = formatFooterFiatFee( + amount = fee.amount.copy(value = fiatFeeValue), + isFeeConvertibleToFiat = feeSelectorUM.feeFiatRateUM != null, + isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate, + appCurrency = appCurrency, + ) + + return if (fee is Fee.Tron) { + getTronTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = resourceReference(R.string.common_nft), + ) + } else { + resourceReference( + id = if (feeSelectorUM.feeFiatRateUM != null) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList( + resourceReference(R.string.common_nft), + fiatFee, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt new file mode 100644 index 0000000000..1fea9dd3ae --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmNotificationsTransformersComparisonTest.kt @@ -0,0 +1,822 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2 + +class NFTSendConfirmNotificationsTransformersComparisonTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN equivalent input data WHEN both transformers transform THEN they produce equal ConfirmUM`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createTestFeeUM() + val feeSelectorUMV2 = createTestFeeSelectorUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.isSending).isEqualTo(contentV2.isSending) + assertThat(contentV1.showTapHelp).isEqualTo(contentV2.showTapHelp) + } + + @Test + fun `GIVEN fee too low WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooLowUM() + val feeSelectorUMV2 = createFeeTooLowUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + // Verify analytics event was sent for both transformers + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN fee too high WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooHighUM() + val feeSelectorUMV2 = createFeeTooHighUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + + val tooHighV1 = contentV1.notifications.first() as NotificationUM.Warning.TooHigh + val tooHighV2 = contentV2.notifications.first() as NotificationUM.Warning.TooHigh + assertThat(tooHighV1.value).isEqualTo(tooHighV2.value) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + @Test + fun `GIVEN fee both too high and too low WHEN both transformers transform THEN they produce equal notifications`() = + runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createFeeTooHighAndTooLowUM() + val feeSelectorUMV2 = createFeeTooHighAndTooLowUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.isPrimaryButtonEnabled).isTrue() + assertThat(contentV2.isPrimaryButtonEnabled).isTrue() + assertThat(contentV1.notifications).hasSize(2) + assertThat(contentV2.notifications).hasSize(2) + + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN normal fee WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + + val feeUM = createNormalFeeUM() + val feeSelectorUMV2 = createNormalFeeSelectorUMV2() + + // WHEN + val transformerV1 = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).isEmpty() + assertThat(contentV2.notifications).isEmpty() + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createTestFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createTestFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt new file mode 100644 index 0000000000..e7f065638a --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerTest.kt @@ -0,0 +1,381 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale + +class NFTSendConfirmationNotificationsTransformerTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = mockk(relaxed = true) + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee UM not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = FeeUM.Empty() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM.Content = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeUM = createNormalFeeUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeUM = createFeeTooHighUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeUM = createFeeTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeUM = createFeeTooHighAndTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformer( + feeUM = feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf(), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt new file mode 100644 index 0000000000..e79d598f5e --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt @@ -0,0 +1,450 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2 +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale + +class NFTSendConfirmationNotificationsTransformerV2Test { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = mockk(relaxed = true) + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee selector not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeSelectorUM = createNormalFeeSelectorUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighAndTooLowUM() + val transformer = NFTSendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf(FeeItem.Market(fee)), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + isPrimaryButtonEnabled = true, + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmNotificationsTransformersComparisonTest.kt similarity index 99% rename from features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt rename to features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmNotificationsTransformersComparisonTest.kt index b07143a6b1..4c7c95d94f 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmNotificationsTransformersComparisonTest.kt @@ -27,7 +27,7 @@ import java.util.Locale import com.tangem.domain.tokens.model.Amount as DomainAmount import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2 -class TransformersComparisonTest { +class SendConfirmNotificationsTransformersComparisonTest { private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) From 54e094abac8c48dec3f74fd61cc33b4bc227349e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:21:54 +0200 Subject: [PATCH 154/165] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 6 ++ .../TokenReceiveNewAnalyticsEvent.kt | 59 +++++++++++++++++++ .../v2/send/analytics/SendAnalyticEvents.kt | 6 ++ .../v2/send/analytics/SendAnalyticHelper.kt | 11 ++++ .../component/DefaultTokenReceiveComponent.kt | 1 + .../component/TokenReceiveAssetsComponent.kt | 1 + .../model/TokenReceiveAssetsModel.kt | 24 ++++++++ .../tokenreceive/model/TokenReceiveModel.kt | 41 +++++++++++-- 8 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 9257e90ce8..79c0d883c0 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -187,6 +187,10 @@ sealed class AnalyticsParam { Pending(value = "Pending"), } + enum class EnsStatus(val value: String) { + EMPTY("Empty"), FULL("Full") + } + companion object Key { const val BLOCKCHAIN = "Blockchain" const val TOKEN_PARAM = "Token" @@ -235,5 +239,7 @@ sealed class AnalyticsParam { const val SEND_BLOCKCHAIN = "Send Blockchain" const val RECEIVE_BLOCKCHAIN = "Receive Blockchain" const val CHOSEN_TOKEN = "Token Chosen" + const val ENS = "ENS" + const val ENS_ADDRESS = "ENS Address" } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt new file mode 100644 index 0000000000..424ac7f642 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveNewAnalyticsEvent.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.tokens.model.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +sealed class TokenReceiveNewAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Token / Receive", event, params) { + + class ReceiveScreenOpened( + token: String, + blockchainName: String, + ensStatus: AnalyticsParam.EnsStatus, + ) : TokenReceiveNewAnalyticsEvent( + event = "Receive Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ENS to ensStatus.value, + ), + ) + + class ButtonCopyAddress( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "Button - Copy Address", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) + + class ButtonCopyEns( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "Button - ENS", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) + + class QrScreenOpened( + token: String, + blockchainName: String, + ) : TokenReceiveNewAnalyticsEvent( + event = "QR Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchainName, + ), + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 3a705b2270..c486cce7a4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam 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_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM @@ -23,6 +24,7 @@ internal sealed class SendAnalyticEvents( val feeType: AnalyticsParam.FeeType, val blockchain: String, val nonceNotEmpty: Boolean, + private val ensStatus: AnalyticsParam.EnsStatus, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = mapOf( @@ -30,6 +32,10 @@ internal sealed class SendAnalyticEvents( FEE_TYPE to feeType.value, BLOCKCHAIN to blockchain, NONCE to nonceNotEmpty.toString().capitalize(), + ENS_ADDRESS to when (ensStatus) { + AnalyticsParam.EnsStatus.EMPTY -> false.toString() + AnalyticsParam.EnsStatus.FULL -> true.toString() + }, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index ab9490ce77..ad01b7a5c9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -28,6 +28,7 @@ internal class SendAnalyticHelper @Inject constructor( feeType = feeType, blockchain = cryptoCurrency.network.name, nonceNotEmpty = feeSelectorUM.nonce != null, + ensStatus = getEnsStatus(sendUM), ), ) analyticsEventHandler.send( @@ -52,4 +53,14 @@ internal class SendAnalyticHelper @Inject constructor( else -> Basic.TransactionSent.MemoType.Null } } + + private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus { + val blockchainAddressForEns = + (sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress + return if (blockchainAddressForEns != null) { + AnalyticsParam.EnsStatus.FULL + } else { + AnalyticsParam.EnsStatus.EMPTY + } + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index e467be69ac..83b0cf3b6f 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -93,6 +93,7 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( notificationConfigs = model.state.value.notificationConfigs, showMemoDisclaimer = model.params.config.showMemoDisclaimer, fullName = model.params.config.cryptoCurrency.network.name, + tokenName = model.getTokenName(), ), ) TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent( diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt index de8c7e2578..7ece6e356d 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/TokenReceiveAssetsComponent.kt @@ -39,5 +39,6 @@ internal class TokenReceiveAssetsComponent( val onDismiss: () -> Unit, val showMemoDisclaimer: Boolean, val fullName: String, + val tokenName: String, ) } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt index 04b0caf856..bf37b011e3 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveAssetsModel.kt @@ -1,10 +1,14 @@ package com.tangem.features.tokenreceive.model import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent +import com.tangem.features.tokenreceive.entity.ReceiveAddress import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -16,10 +20,21 @@ import javax.inject.Inject internal class TokenReceiveAssetsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() + init { + analyticsEventHandler.send( + TokenReceiveNewAnalyticsEvent.ReceiveScreenOpened( + token = params.tokenName, + blockchainName = params.fullName, + ensStatus = configureEnsStatus(), + ), + ) + } + internal val state: StateFlow field = MutableStateFlow( ReceiveAssetsUM( @@ -32,4 +47,13 @@ internal class TokenReceiveAssetsModel @Inject constructor( fullName = params.fullName, ), ) + + private fun configureEnsStatus(): AnalyticsParam.EnsStatus { + val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens } + return if (hasEnsAddress) { + AnalyticsParam.EnsStatus.FULL + } else { + AnalyticsParam.EnsStatus.EMPTY + } + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt index 3313bf1cb9..183d11d046 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.tokenreceive.model import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.push import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -19,6 +20,7 @@ import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.ens.EnsAddress import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase +import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback @@ -35,6 +37,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class TokenReceiveModel @Inject constructor( private val clipboardManager: ClipboardManager, @@ -43,6 +46,7 @@ internal class TokenReceiveModel @Inject constructor( private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), TokenReceiveModelCallback { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -61,11 +65,18 @@ internal class TokenReceiveModel @Inject constructor( } override fun onQrCodeClick(id: Int) { + analyticsEventHandler.send( + TokenReceiveNewAnalyticsEvent.QrScreenOpened( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ), + ) stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id)) } override fun onCopyClick(id: Int) { val addressToCopy = state.value.addresses[id] ?: return + sendCopyActionAnalytic(addressToCopy) clipboardManager.setText(text = addressToCopy.value, isSensitive = true) } @@ -85,6 +96,13 @@ internal class TokenReceiveModel @Inject constructor( } } + internal fun getTokenName(): String { + return when (val asset = params.config.asset) { + Asset.Currency -> params.config.cryptoCurrency.symbol + Asset.NFT -> asset.name + } + } + private fun mapAddresses(addresses: List): ImmutableMap { return buildMap { addresses.mapIndexed { index, model -> @@ -151,10 +169,7 @@ internal class TokenReceiveModel @Inject constructor( title = resourceReference( R.string.receive_bottom_sheet_warning_title, wrappedList( - when (val asset = params.config.asset) { - Asset.Currency -> params.config.cryptoCurrency.symbol - Asset.NFT -> asset.name - }, + getTokenName(), params.config.cryptoCurrency.network.name, ), ), @@ -183,4 +198,22 @@ internal class TokenReceiveModel @Inject constructor( notificationConfigs = getNotifications().toImmutableList(), ) } + + private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) { + val event = when (receiveAddress.type) { + is ReceiveAddress.Type.Default -> { + TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ) + } + ReceiveAddress.Type.Ens -> { + TokenReceiveNewAnalyticsEvent.ButtonCopyEns( + token = getTokenName(), + blockchainName = params.config.cryptoCurrency.network.name, + ) + } + } + analyticsEventHandler.send(event) + } } \ No newline at end of file From b84c9b4f1c94cc5a289891362a48e4f5c3f1db16 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 10:31:37 +0200 Subject: [PATCH 155/165] Updated on 2026-08-14 --- .../impl/DefaultOnboardingNoteComponent.kt | 5 +-- .../impl/DefaultOnboardingTwinComponent.kt | 32 +------------------ 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index 00d3747298..092e847893 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -24,11 +24,10 @@ import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent -import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState +import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute -import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -41,7 +40,6 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -111,7 +109,6 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( mode = OnboardingDoneComponent.Mode.WalletCreated, onDone = { params.onDone() }, ), - tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index 0c4797f1a4..9879c7252b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -2,27 +2,19 @@ package com.tangem.features.onboarding.v2.twin.impl import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder import com.tangem.core.decompose.navigation.inner.InnerNavigationState -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin -import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,18 +23,10 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnboardingTwinComponent.Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingTwinComponent, AppComponentContext by appComponentContext, InnerNavigationHolder { private val model: OnboardingTwinModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = TokenReceiveConfig.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - init { params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) } @@ -58,28 +42,14 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { BackHandler { model.onBack() } - - val bottomSheet by bottomSheetSlot.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() OnboardingTwin( state = state, modifier = modifier, ) - bottomSheet.child?.instance?.BottomSheet() } - private fun bottomSheetChild( - config: TokenReceiveConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - data class TwinInnerNavigationState( override val stackSize: Int, ) : InnerNavigationState { From af7caffba5c63f83648f2a0469fc80f10274ddae Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 09:07:42 +0000 Subject: [PATCH 156/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 418aa40d43..f430a4dd17 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.28-1206" +tangemBlockchainSdk = "develop-1205" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.28-559" +tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 35fd75485cf1741a39249ce80832d7fb3801c6ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 16:17:51 +0700 Subject: [PATCH 157/165] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 9 + core/res/src/main/res/values-ru/strings.xml | 10 + .../src/main/res/values-uk-rUA/strings.xml | 10 + core/res/src/main/res/values/strings.xml | 18 ++ .../archived/ArchivedAccountListModel.kt | 10 +- .../createedit/entity/AccountCreateEditUM.kt | 2 +- .../details/entity/AccountDetailsUM.kt | 2 +- .../wallet-settings/impl/build.gradle.kts | 1 + .../impl/DefaultWalletSettingsComponent.kt | 15 ++ .../preview/PreviewWalletSettingsComponent.kt | 37 ++++ .../entity/WalletSettingsItemUM.kt | 33 ++++ .../model/WalletSettingsModel.kt | 11 +- .../walletsettings/ui/WalletSettingsScreen.kt | 171 ++++++++++++++++-- .../utils/AccountItemsDelegate.kt | 111 ++++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 3 + 15 files changed, 413 insertions(+), 30 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2e061c0bce..43ea6c53bf 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,7 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アーカイブされたアカウント 回復する アーカイブ済み アカウントをアーカイブする @@ -26,6 +27,7 @@ 新しいアカウント アカウントを追加 アカウントを編集 + アカウントを長押しして並べ替える 編集を続ける 破棄 新しいアカウントを破棄してもよろしいですか? @@ -116,6 +118,12 @@ 30秒後に再試行するか、カードまたはリングをスキャンしてください 試行回数が多すぎます お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 + プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。 + プロモーションコードが正常に有効化されました。14日以内に10 USDT相当のビットコインボーナスが付与されます。 + プロモーションコードが有効になりました + このプロモーションコードは既に使用されているため、再度使うことはできません。 + このプロモーションコードは無効であり、有効化できません。 + ボーナスを受け取るにはビットコインアドレスが必要です。ウォレットにビットコインアドレスを追加し、再度アクティベーションをお試しください。 バックアップ処理を開始する 銀行カードまたはその他の支払い方法を使用する @@ -149,6 +157,7 @@ ADAが不足しています。 受け入れる アクセスが拒否されました + アカウント 追加 ポートフォリオに追加 トークンを追加 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index befe0fd2d7..899b661b01 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -59,6 +59,16 @@ Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже. + Ошибка активации + Ваш промокод успешно активирован. Бонус 10 USDT в Bitcoin будет зачислен через 14 дней. + Промокод активирован + Этот промокод уже был использован и не может быть активирован повторно. + Код недоступен + Этот промокод недействителен и не может быть активирован. + Неверный код + Для зачисления бонуса нужен Bitcoin-адрес. Добавьте его в портфель и повторите активацию. + Требуется Bitcoin-адрес Начать резервное копирование Используйте банковскую карту или другие методы оплаты diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 5c7d9f4ef4..21880f895f 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -59,6 +59,16 @@ Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Під час обробки промокоду сталася помилка. Будь ласка, спробуйте пізніше. + Помилка активації + Ваш промокод успішно активовано. Бонус 10 USDT у Bitcoin буде зараховано через 14 днів. + Промокод активовано + Цей промокод уже був використаний і не може бути активований повторно. + Код недоступний + Цей промокод недійсний і не може бути активований. + Невірний код + Для зарахування бонусу потрібна Bitcoin-адреса. Додайте її до портфеля та повторіть активацію. + Потрібна Bitcoin-адреса Почніть процес резервного копіювання Використовуйте банківську картку або інші способи оплати diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dc9ced8f5c..0db4cc4be9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,7 +12,12 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + You cannot create more than %1$s accounts. Archive one to add new. + Can’t add new account + Archived accounts Recover + You’re about to recover “%1$s”. + Recover account Archived Archive account Archive @@ -26,6 +31,8 @@ New account Add account Edit account + %1$s in %2$s + Long tap on an account to reorder accounts Keep Editing Discard Are you sure you want to discard new account? @@ -117,6 +124,12 @@ Please try again in 30 seconds or scan the card or ring Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + An error occurred while processing your promo code. Please try again later. + Your promo code has been successfully activated. A bonus of 10 USDT in Bitcoin will be credited in 14 days. + Promo code activated + This promo code has already been used and cannot be activated again. + This promo code is not valid and cannot be activated. + A Bitcoin address is required to receive the bonus. Please add one to your wallet and retry the activation. Start backup process Use a bank card or other payment methods @@ -152,6 +165,7 @@ Not enough ADA Accept Access denied + Accounts Add Add to portfolio Add token @@ -239,6 +253,10 @@ month Network fee Amount sent will be reduced by %1$s (%2$s) to cover the selected fee level + + %d network + %d networks + Next NFT No diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index a93cdcca2b..95554e161b 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -5,9 +5,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R -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.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase @@ -48,8 +47,11 @@ internal class ArchivedAccountListModel @Inject constructor( ) messageSender.send( DialogMessage( - title = stringReference(account.accountName.value), - message = TextReference.EMPTY, + title = resourceReference(R.string.account_archived_recover_dialog_title), + message = resourceReference( + id = R.string.account_archived_recover_dialog_description, + formatArgs = wrappedList(account.accountName.value), + ), firstActionBuilder = { firstAction }, secondActionBuilder = { secondAction }, ), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 330fc2352f..4b4365ba95 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList -data class AccountCreateEditUM( +internal data class AccountCreateEditUM( val title: TextReference, val account: Account, val colorsState: Colors, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt index c68a1f098a..799c295838 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.account.details.entity import com.tangem.common.ui.account.CryptoPortfolioIconUM -data class AccountDetailsUM( +internal data class AccountDetailsUM( val accountName: String, val accountIcon: CryptoPortfolioIconUM, val onCloseClick: () -> Unit, diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index b69105d2c8..3f34abd5a1 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) + implementation(projects.common.ui) /* Project - Domain */ implementation(projects.domain.legacy) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt index 49f23d0f8c..1cb8c2c145 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.component.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -20,6 +22,7 @@ import com.tangem.feature.walletsettings.entity.DialogConfig import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotificationBSConfig import com.tangem.feature.walletsettings.model.WalletSettingsModel import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -61,6 +64,18 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( dialog = { dialog.child?.instance?.Dialog() }, ) + val requestPushPermission = requestPermission( + onAllow = { state.onPushNotificationPermissionGranted(true) }, + onDeny = { state.onPushNotificationPermissionGranted(false) }, + permission = PUSH_PERMISSION, + ) + + if (state.requestPushNotificationsPermission) { + LaunchedEffect(Unit) { + requestPushPermission() + } + } + bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 1ac9106632..69bf4cfb75 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -2,12 +2,19 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.ui.WalletSettingsScreen import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.hot.sdk.model.HotWalletId @@ -48,11 +55,41 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { walletUpgradeDismissed = false, onUpgradeWalletClick = {}, onDismissUpgradeWalletClick = {}, + accountsUM = previewAccounts(), ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, ) + private fun previewAccounts() = buildList { + WalletSettingsAccountsUM.Header( + id = "accounts_header", + text = resourceReference(R.string.common_accounts), + ).let(::add) + WalletSettingsAccountsUM.Account( + id = "accountId", + accountName = stringReference("Main account"), + accountIconUM = AccountIconPreviewData.randomAccountIcon(), + tokensInfo = stringReference("10 tokens"), + networksInfo = stringReference("2 networks"), + onClick = {}, + ).let(::add) + WalletSettingsAccountsUM.Footer( + id = "accounts_footer", + addAccount = AddAccountUM( + title = resourceReference(R.string.account_form_title_create), + addAccountEnabled = true, + onAddAccountClick = {}, + ), + archivedAccounts = BlockUM( + text = resourceReference(R.string.account_archived_accounts), + iconRes = R.drawable.ic_archive_24, + onClick = {}, + ), + description = resourceReference(R.string.account_reorder_description), + ).let(::add) + } + @Composable override fun Content(modifier: Modifier) { WalletSettingsScreen( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 91a77aec05..85a6bcb784 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.entity import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -51,4 +52,36 @@ internal sealed class WalletSettingsItemUM { val onClick: () -> Unit, val onDismissClick: () -> Unit, ) : WalletSettingsItemUM() +} + +@Immutable +internal sealed class WalletSettingsAccountsUM : WalletSettingsItemUM() { + + data class Header( + override val id: String, + val text: TextReference, + ) : WalletSettingsAccountsUM() + + data class Account( + override val id: String, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val networksInfo: TextReference, + val onClick: () -> Unit, + ) : WalletSettingsAccountsUM() + + data class Footer( + override val id: String, + val addAccount: AddAccountUM, + val archivedAccounts: BlockUM, + val description: TextReference, + ) : WalletSettingsAccountsUM() { + + data class AddAccountUM( + val title: TextReference, + val addAccountEnabled: Boolean, + val onAddAccountClick: () -> Unit, + ) + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index c190745304..1a5e3ee067 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,11 +15,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction @@ -29,6 +25,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.DisableWalletNFTUseCase import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase @@ -36,7 +33,6 @@ import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUse import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.repositories.PermissionRepository -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.* import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents @@ -46,6 +42,7 @@ import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotification import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -65,6 +62,7 @@ internal class WalletSettingsModel @Inject constructor( private val messageSender: UiMessageSender, private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, + private val accountItemsDelegate: AccountItemsDelegate, override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, @@ -224,6 +222,7 @@ internal class WalletSettingsModel @Inject constructor( walletUpgradeDismissed = isUpgradeNotificationEnabled, onUpgradeWalletClick = ::onUpgradeWalletClick, onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick, + accountsUM = with(accountItemsDelegate) { listOf() }, // todo account ) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 8c4bbd6cf2..b18c80f1f0 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -2,20 +2,29 @@ package com.tangem.feature.walletsettings.ui import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountRow +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM @@ -30,12 +39,11 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.WalletSettingsScreenTestTags -import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R -import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @Composable internal fun WalletSettingsScreen( @@ -71,7 +79,6 @@ internal fun WalletSettingsScreen( private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { LazyColumn( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), contentPadding = PaddingValues( top = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16, @@ -89,8 +96,17 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { items = state.items, key = WalletSettingsItemUM::id, ) { item -> - val itemModifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) + val offsetModifier = when (item) { + is WalletSettingsAccountsUM.Account, + is WalletSettingsAccountsUM.Footer, + -> Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + else -> Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + } + val itemModifier = offsetModifier .fillMaxWidth() .testTag(WalletSettingsScreenTestTags.SCREEN_ITEM) @@ -121,21 +137,12 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) + is WalletSettingsAccountsUM.Header -> AccountsHeader(item, itemModifier) + is WalletSettingsAccountsUM.Account -> AccountItem(item, itemModifier) + is WalletSettingsAccountsUM.Footer -> AccountsFooter(item, itemModifier) } } } - - val requestPushPermission = requestPermission( - onAllow = { state.onPushNotificationPermissionGranted(true) }, - onDeny = { state.onPushNotificationPermissionGranted(false) }, - permission = PUSH_PERMISSION, - ) - - if (state.requestPushNotificationsPermission) { - LaunchedEffect(Unit) { - requestPushPermission() - } - } } @Composable @@ -257,6 +264,134 @@ private fun NotificationAlertBlock(model: WalletSettingsItemUM.NotificationPermi ) } +@Composable +private fun AccountsHeader(model: WalletSettingsAccountsUM.Header, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .background( + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ) + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, + ), + text = model.text.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun AccountItem(model: WalletSettingsAccountsUM.Account, modifier: Modifier = Modifier) { + val subtitle = stringResourceSafe( + id = R.string.account_label_tokens_info, + formatArgs = arrayOf( + model.tokensInfo.resolveReference(), + model.networksInfo.resolveReference(), + ), + ) + AccountRow( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .clickable(onClick = model.onClick) + .padding(12.dp), + title = model.accountName, + subtitle = stringReference(subtitle), + icon = model.accountIconUM, + ) +} + +@Composable +private fun AccountsFooter(model: WalletSettingsAccountsUM.Footer, modifier: Modifier = Modifier) { + Column(modifier) { + Column( + modifier = Modifier.background( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ), + color = TangemTheme.colors.background.primary, + ), + ) { + AddAccountRow(model.addAccount) + SpacerH( + height = TangemTheme.dimens.size0_5, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing12) + .background(TangemTheme.colors.stroke.primary), + ) + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = model.archivedAccounts, + ) + } + SpacerH8() + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = model.description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun AddAccountRow(model: WalletSettingsAccountsUM.Footer.AddAccountUM, modifier: Modifier = Modifier) { + val iconTint: Color + val backgroundColor: Color + val textColor: Color + if (model.addAccountEnabled) { + iconTint = TangemTheme.colors.icon.accent + backgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1f) + textColor = TangemTheme.colors.text.accent + } else { + iconTint = TangemTheme.colors.icon.inactive + backgroundColor = TangemTheme.colors.field.primary + textColor = TangemTheme.colors.text.disabled + } + Row( + modifier = modifier + .clickable(onClick = model.onAddAccountClick) + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(10.dp)) + .background(backgroundColor) + .clickable(onClick = { model.onAddAccountClick() }), + ) { + Icon( + modifier = Modifier.size(18.dp), + tint = iconTint, + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_24), + contentDescription = null, + ) + } + + Text( + text = model.title.resolveReference(), + color = textColor, + style = TangemTheme.typography.subtitle1, + ) + } +} + @Composable private fun DescriptionWithMoreBlock( model: WalletSettingsItemUM.DescriptionWithMore, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt new file mode 100644 index 0000000000..7e9939d2e3 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.walletsettings.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.account.toUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM +import com.tangem.feature.walletsettings.impl.R +import javax.inject.Inject + +@ModelScoped +internal class AccountItemsDelegate @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, +) { + + fun buildUiList(userWalletId: UserWalletId, accounts: List): List = buildList { + WalletSettingsAccountsUM.Header( + id = "accounts_header", + text = resourceReference(R.string.common_accounts), + ).let(::add) + + addAll(accounts.map(::mapAccount)) + + val addAccountEnabled = true // todo account + WalletSettingsAccountsUM.Footer( + id = "accounts_footer", + addAccount = AddAccountUM( + title = resourceReference(R.string.account_form_title_create), + addAccountEnabled = addAccountEnabled, + onAddAccountClick = { + if (addAccountEnabled) openAddAccount(userWalletId) else canNotAddAccountDialog() + }, + ), + archivedAccounts = BlockUM( + text = resourceReference(R.string.account_archived_accounts), + iconRes = R.drawable.ic_archive_24, + onClick = { openArchivedAccounts(userWalletId) }, + ), + description = resourceReference(R.string.account_reorder_description), + ).let(::add) + } + + private fun mapAccount(account: Account): WalletSettingsAccountsUM = when (account) { + is Account.CryptoPortfolio -> account.mapCryptoPortfolio() + } + + private fun Account.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM { + return WalletSettingsAccountsUM.Account( + id = accountId.value, + accountName = stringReference(accountName.value), + accountIconUM = icon.toUM(), + tokensInfo = pluralReference( + R.plurals.common_tokens_count, + count = tokensCount, + formatArgs = wrappedList(tokensCount), + ), + networksInfo = pluralReference( + R.plurals.common_networks_count, + count = networksCount, + formatArgs = wrappedList(networksCount), + ), + onClick = { openAccountDetails(this) }, + ) + } + + private fun openAccountDetails(account: Account) { + router.push(AppRoute.AccountDetails(account)) + } + + private fun openArchivedAccounts(userWalletId: UserWalletId) { + router.push(AppRoute.ArchivedAccountList(userWalletId)) + } + + private fun openAddAccount(userWalletId: UserWalletId) { + router.push(AppRoute.CreateAccount(userWalletId)) + } + + private fun canNotAddAccountDialog() { + val firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = { }, + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.account_add_limit_dialog_title), + message = resourceReference( + id = R.string.account_add_limit_dialog_description, + formatArgs = wrappedList(MAX_ACCOUNT_COUNT.toString()), + ), + firstActionBuilder = { firstAction }, + ), + ) + } + + companion object { + // todo account use domain const? + private const val MAX_ACCOUNT_COUNT = 20 + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 4189d78f2a..d26dc7fd98 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings +import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R import com.tangem.hot.sdk.model.HotWalletId @@ -30,6 +31,7 @@ internal class ItemsBuilder @Inject constructor( fun buildItems( userWallet: UserWallet, userWalletName: String, + accountsUM: List, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, @@ -60,6 +62,7 @@ internal class ItemsBuilder @Inject constructor( onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, ), ) + .addAll(accountsUM) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) .add( buildCardItem( From 3e018452037eddaf01e0df62c813d5aeb70cb384 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 16:14:54 +0400 Subject: [PATCH 158/165] Updated on 2026-08-14 --- data/account/build.gradle.kts | 16 +- .../account/converter/AccountConvertersExt.kt | 33 ++++ .../account/converter/AccountListConverter.kt | 46 +++++ .../converter/ArchivedAccountConverter.kt | 29 ++++ .../converter/CryptoPortfolioConverter.kt | 61 +++++++ .../converter/CryptoPortfolioIconConverter.kt | 21 +++ .../GetWalletAccountsResponseConverter.kt | 44 +++++ .../SaveWalletAccountsResponseConverter.kt | 33 ++++ .../converter/TokensGroupTypeConverter.kt | 29 ++++ .../converter/TokensSortTypeConverter.kt | 29 ++++ .../account/converter/AccountConverterExt.kt | 85 ++++++++++ .../converter/AccountListConverterTest.kt | 159 ++++++++++++++++++ .../converter/ArchivedAccountConverterTest.kt | 144 ++++++++++++++++ .../converter/CryptoPortfolioConverterTest.kt | 155 +++++++++++++++++ .../CryptoPortfolioIconConverterTest.kt | 66 ++++++++ .../GetWalletAccountsResponseConverterTest.kt | 130 ++++++++++++++ ...SaveWalletAccountsResponseConverterTest.kt | 50 ++++++ .../converter/TokensGroupTypeConverterTest.kt | 85 ++++++++++ .../converter/TokensSortTypeConverterTest.kt | 81 +++++++++ .../currency/UserTokensResponseFactory.kt | 3 +- .../tangem/domain/models/account/AccountId.kt | 33 ++++ 21 files changed, 1330 insertions(+), 2 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 464ba8486e..287f2b190f 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -9,9 +9,14 @@ android { namespace = "com.tangem.data.account" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { // region Project - Core + implementation(projects.core.datasource) api(projects.core.utils) // endregion @@ -21,7 +26,7 @@ dependencies { // endregion // Project - Data - implementation(projects.core.datasource) + implementation(projects.data.common) // endregion // region DI @@ -34,4 +39,13 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.timber) // endregion + + // region Test + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt new file mode 100644 index 0000000000..ab8eef91a8 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConvertersExt.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +internal fun String.toAccountId(userWalletId: UserWalletId): AccountId { + return AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId).getOrElse { + error("Unable to create AccountId from value: $this. Cause: $it") + } +} + +internal fun String.toAccountName(): AccountName { + return AccountName(value = this).getOrElse { + error("Unable to create AccountName from value: $this. Cause: $it") + } +} + +internal fun WalletAccountDTO.toIcon(): CryptoPortfolioIcon { + return CryptoPortfolioIconConverter.convert( + value = CryptoPortfolioIconConverter.DataModel(icon = icon, color = iconColor), + ) +} + +internal fun Int.toDerivationIndex(): DerivationIndex { + return DerivationIndex(value = this).getOrElse { + error("Unable to create DerivationIndex from value: $this. Cause: $it") + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt new file mode 100644 index 0000000000..d4c993f59f --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -0,0 +1,46 @@ +package com.tangem.data.account.converter + +import arrow.core.getOrElse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [GetWalletAccountsResponse] to an [AccountList] and vice versa + * + * @property userWallet the user wallet associated with the account list + * @param cryptoPortfolioConverterFactory factory to create [CryptoPortfolioConverter] instances + * +[REDACTED_AUTHOR] + */ +internal class AccountListConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: GetWalletAccountsResponse): AccountList { + return AccountList( + userWallet = userWallet, + accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(), + totalAccounts = value.wallet.totalAccounts, + sortType = TokensSortTypeConverter.convert(value.wallet.sort), + groupType = TokensGroupTypeConverter.convert(value.wallet.group), + ) + .getOrElse { + error("Failed to convert GetWalletAccountsResponse to AccountList: $it") + } + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): AccountListConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt new file mode 100644 index 0000000000..072c82e3fc --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/ArchivedAccountConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.converter.Converter + +/** + * Converts a [WalletAccountDTO] to an [ArchivedAccount] + * + * @param userWalletId the ID of the user wallet associated with the account + * +[REDACTED_AUTHOR] + */ +internal class ArchivedAccountConverter( + private val userWalletId: UserWalletId, +) : Converter { + + override fun convert(value: WalletAccountDTO): ArchivedAccount { + return ArchivedAccount( + accountId = value.id.toAccountId(userWalletId = userWalletId), + name = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + tokensCount = value.totalTokens ?: error("Total tokens should not be null"), + networksCount = value.totalNetworks ?: error("Total networks should not be null"), + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt new file mode 100644 index 0000000000..bae26707a6 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.data.account.converter + +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.TwoWayConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Converts a [WalletAccountDTO] to an [Account.CryptoPortfolio] and vise versa + * + * @property userWallet the user wallet associated with the account list + * @property responseCryptoCurrenciesFactory factory to create crypto currencies from response tokens + * +[REDACTED_AUTHOR] + */ +internal class CryptoPortfolioConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val userTokensResponseFactory: UserTokensResponseFactory, +) : TwoWayConverter { + + override fun convert(value: WalletAccountDTO): Account.CryptoPortfolio { + val tokens = value.tokens ?: error("Tokens should not be null") + + return Account.CryptoPortfolio( + accountId = value.id.toAccountId(userWallet.walletId), + accountName = value.name.toAccountName(), + icon = value.toIcon(), + derivationIndex = value.derivationIndex.toDerivationIndex(), + cryptoCurrencies = if (tokens.isNotEmpty()) { + responseCryptoCurrenciesFactory.createCurrencies( + tokens = tokens, + userWallet = userWallet, + ).toSet() + } else { + emptySet() + }, + ) + } + + override fun convertBack(value: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = value.accountId.value, + name = value.accountName.value, + derivationIndex = value.derivationIndex.value, + icon = value.icon.value.name, + iconColor = value.icon.color.name, + tokens = value.cryptoCurrencies.map(userTokensResponseFactory::createResponseToken), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): CryptoPortfolioConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt new file mode 100644 index 0000000000..33b8b2ab20 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioIconConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.account.converter + +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.utils.converter.Converter + +/** + * Converts a [CryptoPortfolioIconConverter.DataModel] to a [CryptoPortfolioIcon] + * +[REDACTED_AUTHOR] + */ +internal object CryptoPortfolioIconConverter : Converter { + + override fun convert(value: DataModel): CryptoPortfolioIcon { + return CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.valueOf(value.icon), + color = CryptoPortfolioIcon.Color.valueOf(value.color), + ) + } + + data class DataModel(val icon: String, val color: String) +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..4ae6bf8b41 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/GetWalletAccountsResponseConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.converter.Converter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** +[REDACTED_AUTHOR] + */ +internal class GetWalletAccountsResponseConverter @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + @Assisted val version: Int, + cryptoPortfolioConverterFactory: CryptoPortfolioConverter.Factory, +) : Converter { + + private val cryptoPortfolioConverter: CryptoPortfolioConverter by lazy { + cryptoPortfolioConverterFactory.create(userWallet) + } + + override fun convert(value: AccountList): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = version, + group = TokensGroupTypeConverter.convertBack(value.groupType), + sort = TokensSortTypeConverter.convertBack(value.sortType), + totalAccounts = value.totalAccounts, + ), + accounts = value.accounts + .filterIsInstance() + .map(cryptoPortfolioConverter::convertBack), + unassignedTokens = emptyList(), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet, version: Int): GetWalletAccountsResponseConverter + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt new file mode 100644 index 0000000000..25c5307b84 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.utils.converter.Converter + +/** + * Converts an [AccountList] to a [SaveWalletAccountsResponse] + * +[REDACTED_AUTHOR] + */ +internal object SaveWalletAccountsResponseConverter : Converter { + + override fun convert(value: AccountList): SaveWalletAccountsResponse { + return SaveWalletAccountsResponse( + accounts = value.accounts + .filterIsInstance() + .map(::toDTO), + ) + } + + private fun toDTO(account: Account.CryptoPortfolio): WalletAccountDTO { + return WalletAccountDTO( + id = account.accountId.value, + name = account.accountName.value, + derivationIndex = account.derivationIndex.value, + icon = account.icon.value.name, + iconColor = account.icon.color.name, + ) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt new file mode 100644 index 0000000000..9a9b92af1b --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensGroupTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.GroupType] to a [TokensGroupType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensGroupTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.GroupType): TokensGroupType { + return when (value) { + UserTokensResponse.GroupType.NETWORK -> TokensGroupType.NETWORK + UserTokensResponse.GroupType.NONE, + UserTokensResponse.GroupType.TOKEN, + -> TokensGroupType.NONE + } + } + + override fun convertBack(value: TokensGroupType): UserTokensResponse.GroupType { + return when (value) { + TokensGroupType.NONE -> UserTokensResponse.GroupType.NONE + TokensGroupType.NETWORK -> UserTokensResponse.GroupType.NETWORK + } + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt new file mode 100644 index 0000000000..91b2b2e88a --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/TokensSortTypeConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import com.tangem.utils.converter.TwoWayConverter + +/** + * Converts a [UserTokensResponse.SortType] to a [TokensSortType] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object TokensSortTypeConverter : TwoWayConverter { + + override fun convert(value: UserTokensResponse.SortType): TokensSortType { + return when (value) { + UserTokensResponse.SortType.BALANCE -> TokensSortType.BALANCE + UserTokensResponse.SortType.MANUAL, + UserTokensResponse.SortType.MARKETCAP, + -> TokensSortType.NONE + } + } + + override fun convertBack(value: TokensSortType): UserTokensResponse.SortType { + return when (value) { + TokensSortType.NONE -> UserTokensResponse.SortType.MANUAL + TokensSortType.BALANCE -> UserTokensResponse.SortType.BALANCE + } + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt new file mode 100644 index 0000000000..5670802776 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +internal fun createWalletAccountDTO( + userWalletId: UserWalletId, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): WalletAccountDTO { + val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) + + return WalletAccountDTO( + id = accountId ?: mainAccount.accountId.value, + name = accountName ?: mainAccount.accountName.value, + derivationIndex = derivationIndex ?: mainAccount.derivationIndex.value, + icon = icon ?: mainAccount.icon.value.name, + iconColor = iconColor ?: mainAccount.icon.color.name, + tokens = tokens, + ) +} + +internal fun createCryptoPortfolio(userWalletId: UserWalletId): Account.CryptoPortfolio { + return Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) +} + +internal fun createGetWalletAccountsResponse( + userWalletId: UserWalletId, + groupType: UserTokensResponse.GroupType = UserTokensResponse.GroupType.NETWORK, + sortType: UserTokensResponse.SortType = UserTokensResponse.SortType.BALANCE, + accountId: String? = null, + accountName: String? = null, + icon: String? = null, + iconColor: String? = null, + derivationIndex: Int? = null, + tokens: List? = emptyList(), +): GetWalletAccountsResponse { + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = groupType, + sort = sortType, + totalAccounts = 1, + ), + accounts = buildList { + createWalletAccountDTO( + userWalletId = userWalletId, + accountId = accountId, + accountName = accountName, + icon = icon, + iconColor = iconColor, + derivationIndex = derivationIndex, + tokens = tokens, + ) + .let(::add) + }, + unassignedTokens = emptyList(), + ) +} + +internal fun createAccountList( + userWallet: UserWallet, + sortType: TokensSortType = TokensSortType.BALANCE, + groupType: TokensGroupType = TokensGroupType.NETWORK, +): AccountList { + return AccountList( + userWallet = userWallet, + accounts = setOf(createCryptoPortfolio(userWallet.walletId)), + totalAccounts = 1, + sortType = sortType, + groupType = groupType, + ) + .getOrNull()!! +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt new file mode 100644 index 0000000000..8b32adb5e5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -0,0 +1,159 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountListConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = AccountListConverter(userWallet, cryptoPortfolioConverterFactory) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val dto = createGetWalletAccountsResponse(userWallet.walletId) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convert(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(dto) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { dto -> + val account = model.expected.getOrNull()!!.accounts + .firstOrNull { it.accountId.value == dto.id } as? Account.CryptoPortfolio + + every { cryptoPortfolioConverter.convert(dto) } returns account!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.TOKEN, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MARKETCAP, + groupType = UserTokensResponse.GroupType.NONE, + ), + expected = Result.success( + createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + ), + ), + ConvertModel( + value = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 1, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ), + expected = Result.failure( + IllegalStateException( + "Failed to convert GetWalletAccountsResponse to AccountList: EmptyAccountsList: " + + "The accounts list cannot be empty", + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: GetWalletAccountsResponse, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt new file mode 100644 index 0000000000..90831c1d3b --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/ArchivedAccountConverterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArchivedAccountConverterTest { + + private val userWalletId = UserWalletId("011") + private val converter = ArchivedAccountConverter(userWalletId = userWalletId) + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { converter.convert(value = model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = createDTO(), + expected = Result.success(createDomain()), + ), + TestModel( + value = createDTO(accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + TestModel( + value = createDTO(name = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + TestModel( + value = createDTO(icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = createDTO(iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + TestModel( + value = createDTO(derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + TestModel( + value = createDTO(totalTokens = null), + expected = Result.failure( + IllegalStateException("Total tokens should not be null"), + ), + ), + TestModel( + value = createDTO(totalNetworks = null), + expected = Result.failure( + IllegalStateException("Total networks should not be null"), + ), + ), + ) + } + + private fun createDTO( + accountId: String = "957B88B12730E646E0F33D3618B77DFA579E8231E3C59C7104BE7165611C8027", + name: String = "Test Account", + icon: String = "Letter", + iconColor: String = "Azure", + derivationIndex: Int = 0, + totalTokens: Int? = 1, + totalNetworks: Int? = 1, + ): WalletAccountDTO { + return WalletAccountDTO( + id = accountId, + name = name, + derivationIndex = derivationIndex, + icon = icon, + iconColor = iconColor, + tokens = null, + totalTokens = totalTokens, + totalNetworks = totalNetworks, + ) + } + + private fun createDomain(): ArchivedAccount { + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio(userWalletId, DerivationIndex(0).getOrNull()!!), + name = "Test Account".toAccountName(), + derivationIndex = 0.toDerivationIndex(), + icon = CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + tokensCount = 1, + networksCount = 1, + ) + } + + data class TestModel( + val value: WalletAccountDTO, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt new file mode 100644 index 0000000000..f046bae96a --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioConverterTest.kt @@ -0,0 +1,155 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class CryptoPortfolioConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk() + private val userTokensResponseFactory: UserTokensResponseFactory = mockk() + private val converter = CryptoPortfolioConverter( + userWallet = userWallet, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + userTokensResponseFactory = userTokensResponseFactory, + ) + + @BeforeEach + fun setupEach() { + clearMocks(responseCryptoCurrenciesFactory, userTokensResponseFactory) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId), + expected = Result.success(createCryptoPortfolio(userWalletId = userWallet.walletId)), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountId = "123"), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountId from value: 123. Cause: ${AccountId.Error.InvalidFormat}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, accountName = ""), + expected = Result.failure( + IllegalStateException( + "Unable to create AccountName from value: . Cause: ${AccountName.Error.Empty}", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, icon = "INVALID_ICON"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, iconColor = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, derivationIndex = -1), + expected = Result.failure( + IllegalStateException( + "Unable to create DerivationIndex from value: -1. " + + "Cause: NegativeDerivationIndex: Derivation index cannot be negative: -1", + ), + ), + ), + ConvertModel( + value = createWalletAccountDTO(userWalletId = userWallet.walletId, tokens = null), + expected = Result.failure( + IllegalStateException("Tokens should not be null"), + ), + ), + ) + } + } + + data class ConvertModel( + val value: WalletAccountDTO, + val expected: Result, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = converter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = createCryptoPortfolio(userWalletId = userWallet.walletId), + expected = createWalletAccountDTO(userWalletId = userWallet.walletId), + ), + ) + } + } + + data class ConvertBackModel( + val value: Account.CryptoPortfolio, + val expected: WalletAccountDTO, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt new file mode 100644 index 0000000000..c6563f9fc8 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/CryptoPortfolioIconConverterTest.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.account.converter.CryptoPortfolioIconConverter.DataModel +import com.tangem.domain.models.account.CryptoPortfolioIcon +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class CryptoPortfolioIconConverterTest { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: TestModel) { + // Act + val actual = runCatching { CryptoPortfolioIconConverter.convert(model.value) } + + // Assert + actual + .onSuccess { + val expected = model.expected.getOrNull()!! + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull()!! + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + TestModel( + value = DataModel(icon = "Letter", color = "Azure"), + expected = Result.success( + CryptoPortfolioIcon.ofCustomAccount( + value = CryptoPortfolioIcon.Icon.Letter, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + ), + TestModel( + value = DataModel(icon = "INVALID_ICON", color = "Azure"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Icon.INVALID_ICON", + ), + ), + ), + TestModel( + value = DataModel(icon = "Letter", color = "INVALID_COLOR"), + expected = Result.failure( + IllegalArgumentException( + "No enum constant com.tangem.domain.models.account.CryptoPortfolioIcon.Color.INVALID_COLOR", + ), + ), + ), + ) + } + + data class TestModel( + val value: DataModel, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..b6dddb7aa5 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt @@ -0,0 +1,130 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetWalletAccountsResponseConverterTest { + + private val userWallet = mockk { + every { walletId } returns UserWalletId("011") + } + private val cryptoPortfolioConverterFactory = mockk() + private val cryptoPortfolioConverter = mockk() + private val converter = GetWalletAccountsResponseConverter( + userWallet = userWallet, + version = 0, + cryptoPortfolioConverterFactory = cryptoPortfolioConverterFactory, + ) + + @BeforeAll + fun setupAll() { + every { cryptoPortfolioConverterFactory.create(userWallet) } returns cryptoPortfolioConverter + } + + @BeforeEach + fun setupEach() { + clearMocks(cryptoPortfolioConverter) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @Test + fun `cryptoPortfolioConverter throws exception`() { + // Arrange + val domain = createAccountList(userWallet = userWallet) + val exception = IllegalStateException("Test exception") + + every { cryptoPortfolioConverter.convertBack(any()) } throws exception + + // Act + val actual = runCatching { converter.convert(domain) }.exceptionOrNull()!! + + // Asset + val expected = exception + Truth.assertThat(actual).isInstanceOf(expected::class.java) + Truth.assertThat(actual.message).isEqualTo(expected.message) + } + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Arrange + if (model.expected.isSuccess) { + model.value.accounts.forEach { domain -> + val dto = model.expected.getOrNull()!!.accounts.firstOrNull { it.id == domain.accountId.value } + + every { cryptoPortfolioConverter.convertBack(domain as Account.CryptoPortfolio) } returns dto!! + } + } + + // Act + val actual = runCatching { converter.convert(model.value) } + + // Asset + actual + .onSuccess { + val expected = model.expected.getOrNull() + Truth.assertThat(it).isEqualTo(expected) + } + .onFailure { + val expected = model.expected.exceptionOrNull() ?: throw it + Truth.assertThat(it).isInstanceOf(expected::class.java) + Truth.assertThat(it.message).isEqualTo(expected.message) + } + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.BALANCE, + groupType = TokensGroupType.NETWORK, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.BALANCE, + groupType = UserTokensResponse.GroupType.NETWORK, + ), + ), + ), + ConvertModel( + value = createAccountList( + userWallet = userWallet, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ), + expected = Result.success( + createGetWalletAccountsResponse( + userWalletId = userWallet.walletId, + sortType = UserTokensResponse.SortType.MANUAL, + groupType = UserTokensResponse.GroupType.NONE, + ), + ), + ), + ) + } + } + + data class ConvertModel( + val value: AccountList, + val expected: Result, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt new file mode 100644 index 0000000000..13ad114fe4 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt @@ -0,0 +1,50 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SaveWalletAccountsResponseConverterTest { + + @Test + fun convert() { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns UserWalletId("011") + } + + val accountList = AccountList( + userWallet = userWallet, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + totalAccounts = 1, + ) + .getOrNull()!! + + // Act + val actual = SaveWalletAccountsResponseConverter.convert(value = accountList) + + // Assert + val expected = SaveWalletAccountsResponse( + accounts = listOf( + WalletAccountDTO( + id = accountList.mainAccount.accountId.value, + name = accountList.mainAccount.accountName.value, + derivationIndex = accountList.mainAccount.derivationIndex.value, + icon = accountList.mainAccount.icon.value.name, + iconColor = accountList.mainAccount.icon.color.name, + ), + ), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt new file mode 100644 index 0000000000..62ba8b6f30 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensGroupTypeConverterTest.kt @@ -0,0 +1,85 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensGroupType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensGroupTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensGroupTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertModel( + value = UserTokensResponse.GroupType.NETWORK, + expected = TokensGroupType.NETWORK, + ), + ConvertModel( + value = UserTokensResponse.GroupType.NONE, + expected = TokensGroupType.NONE, + ), + ConvertModel( + value = UserTokensResponse.GroupType.TOKEN, + expected = TokensGroupType.NONE, + ), + ) + } + } + + data class ConvertModel( + val value: UserTokensResponse.GroupType, + val expected: TokensGroupType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensGroupTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels(): List { + return listOf( + ConvertBackModel( + value = TokensGroupType.NETWORK, + expected = UserTokensResponse.GroupType.NETWORK, + ), + ConvertBackModel( + value = TokensGroupType.NONE, + expected = UserTokensResponse.GroupType.NONE, + ), + ) + } + } + + data class ConvertBackModel( + val value: TokensGroupType, + val expected: UserTokensResponse.GroupType, + ) +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt new file mode 100644 index 0000000000..e5476f5c49 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/converter/TokensSortTypeConverterTest.kt @@ -0,0 +1,81 @@ +package com.tangem.data.account.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.models.TokensSortType +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TokensSortTypeConverterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Convert { + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertModel) { + // Act + val actual = TokensSortTypeConverter.convert(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertModel( + value = UserTokensResponse.SortType.BALANCE, + expected = TokensSortType.BALANCE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MANUAL, + expected = TokensSortType.NONE, + ), + ConvertModel( + value = UserTokensResponse.SortType.MARKETCAP, + expected = TokensSortType.NONE, + ), + ) + } + + data class ConvertModel( + val value: UserTokensResponse.SortType, + val expected: TokensSortType, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ConvertBack { + + @ParameterizedTest + @ProvideTestModels + fun convertBack(model: ConvertBackModel) { + // Act + val actual = TokensSortTypeConverter.convertBack(model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + fun provideTestModels() = listOf( + ConvertBackModel( + value = TokensSortType.BALANCE, + expected = UserTokensResponse.SortType.BALANCE, + ), + ConvertBackModel( + value = TokensSortType.NONE, + expected = UserTokensResponse.SortType.MANUAL, + ), + ) + } + + data class ConvertBackModel( + val value: TokensSortType, + val expected: UserTokensResponse.SortType, + ) +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 4c2623149b..84b75233ac 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -2,8 +2,9 @@ package com.tangem.data.common.currency import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.models.currency.CryptoCurrency +import javax.inject.Inject -class UserTokensResponseFactory { +class UserTokensResponseFactory @Inject constructor() { fun createUserTokensResponse( currencies: List, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index a9cf874078..3133206dd3 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -1,5 +1,8 @@ package com.tangem.domain.models.account +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure import com.tangem.common.extensions.toByteArray import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.toHexString @@ -18,9 +21,39 @@ data class AccountId private constructor( val userWalletId: UserWalletId, ) { + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "AccountId.Error" + + data object Empty : Error { + override fun toString(): String = "$tag: Account ID cannot be blank" + } + + data object InvalidFormat : Error { + override fun toString(): String = "$tag: Account ID must be a 64-character hexadecimal string" + } + } + companion object { private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") } + private val hexRegex = Regex("^[a-fA-F0-9]{64}$") + + /** + * Creates a unique account identifier for a crypto portfolio + * + * @param userWalletId the identifier of the user wallet + * @param value the unique string value representing the account + * + * @return an [Either] containing the [AccountId] on success, or an [Error] on failure + */ + fun forCryptoPortfolio(userWalletId: UserWalletId, value: String): Either = either { + ensure(value.isNotBlank()) { Error.Empty } + ensure(value.matches(hexRegex)) { Error.InvalidFormat } + + AccountId(value = value, userWalletId = userWalletId) + } /** * Creates a unique account identifier for a crypto portfolio From adfbf8d277b7afb54fc7d5eeaf2adc146741a23c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 10:38:49 +0500 Subject: [PATCH 159/165] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheetWithFooter.kt | 24 +++++++++----- .../v2/feeselector/model/FeeSelectorModel.kt | 3 ++ .../ui/FeeSelectorModalBottomSheet.kt | 31 +++++++++++++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 9964ac424d..3826cc503f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.bottomsheets.modal import android.content.res.Configuration +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -49,7 +50,7 @@ inline fun TangemModalBottomSheetWi noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current @@ -84,7 +85,7 @@ inline fun DefaultModalBottomSheetW noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { var isVisible by remember { mutableStateOf(value = config.isShown) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) @@ -118,7 +119,7 @@ inline fun PreviewModalBottomSheetW skipPartiallyExpanded: Boolean = true, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable BoxScope.(T) -> Unit, + noinline footer: @Composable (BoxScope.(T) -> Unit)?, ) { BasicModalBottomSheetWithFooter( config = config, @@ -145,7 +146,7 @@ inline fun BasicModalBottomSheetWit noinline onBack: (() -> Unit)? = null, crossinline title: @Composable BoxScope.(T) -> Unit, crossinline content: @Composable (T) -> Unit, - crossinline footer: @Composable (BoxScope.(T) -> Unit), + noinline footer: @Composable (BoxScope.(T) -> Unit)?, modifier: Modifier = Modifier, ) { val model = config.content as? T ?: return @@ -156,8 +157,13 @@ inline fun BasicModalBottomSheetWit val scrollState = rememberScrollState(initial = initial) val isKeyboardOpen by rememberIsKeyboardVisible() - val buttonHeight = TangemTheme.dimens.spacing80 - val contentBottomPadding = TangemTheme.dimens.spacing80 + val buttonHeight by animateDpAsState( + if (footer != null) { + 80.dp + } else { + 0.dp + }, + ) // Offset calculation for keyboard scroll adjustment: // 1) Button height (footer) // 2) Column content bottom padding @@ -202,7 +208,7 @@ inline fun BasicModalBottomSheetWit Column( modifier = Modifier .verticalScroll(state = scrollState) - .padding(bottom = contentBottomPadding), + .padding(bottom = buttonHeight), ) { content(model) } @@ -218,7 +224,9 @@ inline fun BasicModalBottomSheetWit .height(buttonHeight) .align(Alignment.BottomCenter), ) { - footer(model) + if (footer != null) { + footer(model) + } } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 2784044d44..07e8e30c81 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -127,6 +127,9 @@ internal class FeeSelectorModel @Inject constructor( ) } uiState.update(FeeItemSelectedTransformer(feeItem)) + if (feeItem !is FeeItem.Custom) { + onDoneClick() + } } override fun onCustomFeeValueChange(index: Int, value: String) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index c99b647b80..d9aea80f8c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -82,15 +82,19 @@ internal fun FeeSelectorModalBottomSheet( modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, - footer = { - PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - enabled = state.isPrimaryButtonEnabled, - text = stringResourceSafe(R.string.common_done), - onClick = feeSelectorIntents::onDoneClick, - ) + footer = if (state.selectedFeeItem is FeeItem.Custom) { + { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + enabled = state.isPrimaryButtonEnabled, + text = stringResourceSafe(R.string.common_done), + onClick = feeSelectorIntents::onDoneClick, + ) + } + } else { + null }, ) } @@ -426,7 +430,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), - selectedFeeItem = customFeeItem, + selectedFeeItem = FeeItem.Slow( + fee = Fee.Common( + Amount( + value = BigDecimal("0.01"), + blockchain = Blockchain.Ethereum, + ), + ), + ), feeExtraInfo = FeeExtraInfo( isFeeApproximate = true, isFeeConvertibleToFiat = true, From 92b55cadbf7bd49ef0a31292b6085676c56e41ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 12:05:42 +0300 Subject: [PATCH 160/165] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f430a4dd17..c57f7b4239 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-557" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-461" +tangemHotSdk = "develop-525" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 1d97ce5203d8b25430a59d4d1e605182054aa713 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 14:35:20 +0500 Subject: [PATCH 161/165] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + .../main/res/drawable/ic_knight_shield_24.xml | 9 + .../res/drawable/ic_mobile_security_24.xml | 12 ++ .../src/main/res/drawable/ic_protect_24.xml | 9 + .../ui/src/main/res/drawable/ic_tangem_64.xml | 18 ++ .../hotwallet/UpgradeWalletComponent.kt | 14 ++ .../DefaultUpgradeWalletComponent.kt | 39 +++++ .../upgradewallet/UpgradeWalletModel.kt | 41 +++++ .../upgradewallet/di/UpgradeWalletModule.kt | 25 +++ .../upgradewallet/entity/UpgradeWalletUM.kt | 7 + .../upgradewallet/ui/UpgradeWalletContent.kt | 161 ++++++++++++++++++ 12 files changed, 351 insertions(+) create mode 100644 core/ui/src/main/res/drawable/ic_knight_shield_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_mobile_security_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_protect_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_tangem_64.xml create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 615b1c3778..9a80508e7b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -17,6 +17,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent +import com.tangem.features.hotwallet.UpgradeWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.CreateWalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent @@ -103,6 +104,7 @@ internal class ChildFactory @Inject constructor( private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory, private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, + private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val walletActivationComponentFactory: WalletActivationComponent.Factory, private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, @@ -486,6 +488,15 @@ internal class ChildFactory @Inject constructor( componentFactory = createMobileWalletComponentFactory, ) } + is AppRoute.UpgradeWallet -> { + createComponentChild( + context = context, + params = UpgradeWalletComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = upgradeWalletComponentFactory, + ) + } is AppRoute.AddExistingWallet -> { createComponentChild( context = context, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index d0ffcca403..cfa1070c25 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -311,6 +311,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet") + @Serializable + data class UpgradeWallet( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/upgrade_wallet/${userWalletId.stringValue}") + @Serializable object AddExistingWallet : AppRoute(path = "/add_existing_wallet") diff --git a/core/ui/src/main/res/drawable/ic_knight_shield_24.xml b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml new file mode 100644 index 0000000000..1a058ff367 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_knight_shield_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_mobile_security_24.xml b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml new file mode 100644 index 0000000000..bbc69392d5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_security_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_protect_24.xml b/core/ui/src/main/res/drawable/ic_protect_24.xml new file mode 100644 index 0000000000..5ad478076f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_protect_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tangem_64.xml b/core/ui/src/main/res/drawable/ic_tangem_64.xml new file mode 100644 index 0000000000..6c39da496c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tangem_64.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt new file mode 100644 index 0000000000..8a9b7f6c14 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/UpgradeWalletComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface UpgradeWalletComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt new file mode 100644 index 0000000000..2f0e91d1de --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/DefaultUpgradeWalletComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.hotwallet.upgradewallet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.ui.UpgradeWalletContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultUpgradeWalletComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: UpgradeWalletComponent.Params, +) : UpgradeWalletComponent, AppComponentContext by context { + + private val model: UpgradeWalletModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + UpgradeWalletContent( + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : UpgradeWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: UpgradeWalletComponent.Params, + ): DefaultUpgradeWalletComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt new file mode 100644 index 0000000000..bc03e54e43 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -0,0 +1,41 @@ +package com.tangem.features.hotwallet.upgradewallet + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class UpgradeWalletModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + internal val uiState: StateFlow + field = MutableStateFlow( + UpgradeWalletUM( + onBackClick = { router.pop() }, + onBuyTangemWalletClick = ::onBuyTangemWalletClick, + onScanDeviceClick = ::onScanDeviceClick, + ), + ) + + private fun onBuyTangemWalletClick() { + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanDeviceClick() { + // TODO [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt new file mode 100644 index 0000000000..3beb13bcab --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/di/UpgradeWalletModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.upgradewallet.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.UpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.DefaultUpgradeWalletComponent +import com.tangem.features.hotwallet.upgradewallet.UpgradeWalletModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface UpgradeWalletModule { + + @Binds + fun bindUpgradeWalletComponentFactory(impl: DefaultUpgradeWalletComponent.Factory): UpgradeWalletComponent.Factory + + @Binds + @IntoMap + @ClassKey(UpgradeWalletModel::class) + fun bindUpgradeWalletModel(model: UpgradeWalletModel): Model +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt new file mode 100644 index 0000000000..8f700a27b9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/entity/UpgradeWalletUM.kt @@ -0,0 +1,7 @@ +package com.tangem.features.hotwallet.upgradewallet.entity + +internal data class UpgradeWalletUM( + val onBackClick: () -> Unit, + val onBuyTangemWalletClick: () -> Unit, + val onScanDeviceClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt new file mode 100644 index 0000000000..2ae6ae5d0c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/ui/UpgradeWalletContent.kt @@ -0,0 +1,161 @@ +package com.tangem.features.hotwallet.upgradewallet.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.upgradewallet.entity.UpgradeWalletUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .systemBarsPadding(), + ) { + TangemTopAppBar( + modifier = Modifier + .statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.onBackClick), + title = TextReference.EMPTY, + ) + Column( + modifier = Modifier + .weight(1f) + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + ) { + Icon( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(R.drawable.ic_tangem_64), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 16.dp, + top = 20.dp, + end = 16.dp, + ), + text = stringResourceSafe(R.string.hw_upgrade_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 32.dp), + title = stringResourceSafe(R.string.hw_upgrade_key_migration_title), + description = stringResourceSafe(R.string.hw_upgrade_key_migration_description), + iconRes = R.drawable.ic_mobile_security_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_funds_access_title), + description = stringResourceSafe(R.string.hw_upgrade_funds_access_description), + iconRes = R.drawable.ic_knight_shield_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.hw_upgrade_general_security_title), + description = stringResourceSafe(R.string.hw_upgrade_general_security_description), + iconRes = R.drawable.ic_protect_24, + ) + } + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + SecondaryButton( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.details_buy_wallet), + onClick = state.onBuyTangemWalletClick, + ) + PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth(), + text = stringResourceSafe(R.string.hw_upgrade_scan_device), + onClick = state.onScanDeviceClick, + iconResId = R.drawable.ic_tangem_24, + ) + } + } +} + +@Composable +private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + ) { + Icon( + modifier = Modifier + .padding(horizontal = 12.dp), + painter = painterResource(iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(top = 4.dp), + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewUpgradeWalletContent() { + TangemThemePreview { + UpgradeWalletContent( + state = UpgradeWalletUM( + onBackClick = {}, + onBuyTangemWalletClick = {}, + onScanDeviceClick = {}, + ), + ) + } +} \ No newline at end of file From 220a43c1b9d38be882dd718a998f32cde379c6f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 14:36:55 +0500 Subject: [PATCH 162/165] Updated on 2026-08-14 --- .../components/notifications/Notification.kt | 1 + .../notifications/NotificationConfig.kt | 1 + .../hotwallet/accesscode/ui/AccessCode.kt | 2 +- .../hotwallet/common/ui/OptionBlock.kt | 2 +- .../walletbackup/entity/WalletBackupUM.kt | 5 +- .../walletbackup/model/WalletBackupModel.kt | 10 ++-- .../walletbackup/ui/WalletBackupContent.kt | 19 ++++---- .../common/preview/WalletScreenPreviewData.kt | 11 ++++- .../domain/GetMultiWalletWarningsFactory.kt | 46 ++++++++++++++++--- .../wallet/state/model/WalletNotification.kt | 11 +++-- .../components/common/WalletNotifications.kt | 6 +++ 11 files changed, 86 insertions(+), 28 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 06a2bc1d9e..a3e97923bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -62,6 +62,7 @@ fun Notification( NotificationConfig.IconTint.Unspecified -> null NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention + NotificationConfig.IconTint.Warning -> TangemTheme.colors.icon.warning }, isEnabled: Boolean = true, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index ca5cdc8e5a..bd4d9dacef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -65,5 +65,6 @@ data class NotificationConfig( Unspecified, Accent, Attention, + Warning, } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 3924d978c7..4596985307 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -74,7 +74,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = !state.isConfirmMode, + isPasswordVisual = state.isConfirmMode, value = state.accessCode, pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt index 8e90402e09..c41dba03e8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/common/ui/OptionBlock.kt @@ -61,7 +61,7 @@ internal fun OptionBlock( color = backgroundColor, shape = TangemTheme.shapes.roundedCornersXMedium, ) - .conditional(onClick != null) { + .conditional(onClick != null && enabled) { onClick?.let { clickableSingle(onClick = it) } ?: Modifier } .padding(16.dp), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index f3f17cd9d3..3bc262d1a9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -4,8 +4,9 @@ import com.tangem.core.ui.components.label.entity.LabelUM internal data class WalletBackupUM( val onBackClick: () -> Unit, - val recoveryPhraseStatus: LabelUM?, - val googleDriveStatus: LabelUM?, + val recoveryPhraseOption: LabelUM?, + val googleDriveOption: LabelUM?, + val googleDriveStatus: BackupStatus, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, val backedUp: Boolean, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index b398ed047a..9996c7fb7a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -40,14 +41,15 @@ internal class WalletBackupModel @Inject constructor( field = MutableStateFlow( WalletBackupUM( onBackClick = { router.pop() }, - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + googleDriveStatus = BackupStatus.ComingSoon, onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, backedUp = false, @@ -95,7 +97,7 @@ internal class WalletBackupModel @Inject constructor( } private fun WalletBackupUM.updateBackupStatusesHotWallet(userWallet: UserWallet.Hot): WalletBackupUM = copy( - recoveryPhraseStatus = if (userWallet.backedUp) { + recoveryPhraseOption = if (userWallet.backedUp) { LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, @@ -106,7 +108,7 @@ internal class WalletBackupModel @Inject constructor( style = LabelStyle.WARNING, ) }, - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index f5d25b7280..671c8dd89a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -51,7 +51,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_seed_title), description = stringResourceSafe(R.string.hw_backup_seed_description), badge = { - state.recoveryPhraseStatus?.let { Label(it) } + state.recoveryPhraseOption?.let { Label(it) } }, onClick = state.onRecoveryPhraseClick, enabled = true, @@ -63,7 +63,7 @@ internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Mod title = stringResourceSafe(R.string.hw_backup_google_drive_title), description = stringResourceSafe(R.string.hw_backup_google_drive_description), badge = { - state.googleDriveStatus?.let { Label(it) } + state.googleDriveOption?.let { Label(it) } }, onClick = state.onGoogleDriveClick, enabled = state.googleDriveStatus != BackupStatus.ComingSoon, @@ -85,42 +85,45 @@ private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider: private class WalletBackupUMProvider : CollectionPreviewParameterProvider( collection = listOf( WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + googleDriveStatus = BackupStatus.ComingSoon, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.hw_backup_no_backup), style = LabelStyle.WARNING, ), + googleDriveStatus = BackupStatus.NoBackup, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, backedUp = false, ), WalletBackupUM( - recoveryPhraseStatus = LabelUM( + recoveryPhraseOption = LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, ), - googleDriveStatus = LabelUM( + googleDriveOption = LabelUM( text = resourceReference(R.string.common_done), style = LabelStyle.ACCENT, ), + googleDriveStatus = BackupStatus.Done, onBackClick = {}, onRecoveryPhraseClick = {}, onGoogleDriveClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index d57ed3079f..9e485dad2e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo @@ -117,7 +120,13 @@ internal object WalletScreenPreviewData { buttons = persistentListOf(buyButton), warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, - WalletNotification.FinishWalletActivation { }, + WalletNotification.FinishWalletActivation( + iconTint = NotificationConfig.IconTint.Attention, + buttonsState = ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = { }, + ), + ), ), bottomSheetConfig = null, tokensListState = textContentTokensState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 248f54a3e6..a7e173397b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce @@ -19,7 +22,9 @@ import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.utils.extensions.isPositive import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -54,7 +59,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) - addFinishWalletActivationNotification(userWallet, clickIntents) + addFinishWalletActivationNotification(userWallet, maybeTokenList, clickIntents) addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) @@ -256,26 +261,55 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { - if (condition) add(element = element) - } - private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, + maybeTokenList: Lce, clickIntents: WalletClickIntents, ) { if (userWallet !is UserWallet.Hot) return val shouldShowFinishActivation = !userWallet.backedUp + val iconTint = maybeTokenList.fold( + ifLoading = { + if ((it?.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifContent = { + if ((it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isPositive() == true) { + IconTint.Warning + } else { + IconTint.Attention + } + }, + ifError = { IconTint.Attention }, + ) + addIf( element = WalletNotification.FinishWalletActivation( - onFinishClick = clickIntents::onFinishWalletActivationClick, + iconTint = iconTint, + buttonsState = when (iconTint) { + IconTint.Warning -> ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + else -> ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = clickIntents::onFinishWalletActivationClick, + ) + }, ), condition = shouldShowFinishActivation, ) } + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) add(element = element) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 3d23c93ae5..dd6ae1ec2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference @@ -257,16 +259,15 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class FinishWalletActivation( - val onFinishClick: () -> Unit, + val iconTint: IconTint, + val buttonsState: ButtonsState, ) : WalletNotification( config = NotificationConfig( title = resourceReference(R.string.hw_activation_need_title), subtitle = resourceReference(R.string.hw_activation_need_description), iconResId = R.drawable.img_knight_shield_32, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.hw_activation_need_finish), - onClick = onFinishClick, - ), + iconTint = iconTint, + buttonsState = buttonsState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 211ce57e2d..172ab84fbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -34,6 +34,12 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + Notification( + config = it.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + ) + } else -> { Notification( config = it.config, From 3a96b203e0171f2375ddc0b31c1c74f30381055f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Aug 2025 19:14:43 +0700 Subject: [PATCH 163/165] Updated on 2026-08-14 --- .../tangem/common/ui/account/AccountRow.kt | 5 ++ .../converter/UserWalletItemUMConverter.kt | 17 +--- features/markets/impl/build.gradle.kts | 1 + .../model/AddToPortfolioBSContentUMFactory.kt | 7 +- .../impl/model/MarketsPortfolioModel.kt | 59 ++++++------ .../impl/model/MyPortfolioUMFactory.kt | 6 +- .../wallet-settings/impl/build.gradle.kts | 2 + .../preview/PreviewWalletSettingsComponent.kt | 15 +++- .../entity/WalletSettingsItemUM.kt | 4 +- .../model/WalletSettingsModel.kt | 36 +++----- .../walletsettings/ui/WalletSettingsScreen.kt | 50 +++++++---- .../walletsettings/utils/ItemsBuilder.kt | 18 +--- .../utils/WalletCardItemDelegate.kt | 55 ++++++++++++ features/wallet/api/build.gradle.kts | 3 + .../wallet/utils/UserWalletImageFetcher.kt | 20 +++++ .../feature/wallet/di/WalletFeatureModule.kt | 7 ++ .../utils/DefaultUserWalletImageFetcher.kt | 89 +++++++++++++++++++ .../wallet/utils/DefaultUserWalletsFetcher.kt | 27 +----- 18 files changed, 285 insertions(+), 136 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt create mode 100644 features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt index c48fa40072..d02fe62c15 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference @@ -73,6 +74,8 @@ private fun Title(title: TextReference) { text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } @@ -82,6 +85,8 @@ private fun Subtitle(subtitle: TextReference) { color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, text = subtitle.resolveReference(), + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 41f2c3611e..41b0727d2d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter import com.tangem.common.ui.R import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.label.entity.LabelStyle +import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -12,7 +12,6 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet @@ -37,10 +36,10 @@ class UserWalletItemUMConverter( private val isBalanceHidden: Boolean = false, private val authMode: Boolean = false, private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, - private val artwork: ArtworkModel? = null, + artwork: UserWalletItemUM.ImageState? = null, ) : Converter { - private val artworkUMConverter = ArtworkUMConverter() + private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading override fun convert(value: UserWallet): UserWalletItemUM { return with(value) { @@ -52,7 +51,7 @@ class UserWalletItemUMConverter( isEnabled = isEnabled(userWallet = this), endIcon = endIcon, onClick = { onClick(value.walletId) }, - imageState = getImageState(userWallet = value), + imageState = artwork, label = getLabelOrNull(userWallet = this), ) } @@ -73,14 +72,6 @@ class UserWalletItemUMConverter( } } - private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState { - return when { - userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet - artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork)) - else -> UserWalletItemUM.ImageState.Loading - } - } - private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 1b9562b290..61d498c799 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { api(projects.features.onramp.api) api(projects.features.sendV2.api) api(projects.features.tokenRecieve.api) + api(projects.features.wallet.api) /* Data */ implementation(projects.data.common) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt index feef86486d..49b7fead68 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -56,7 +55,7 @@ internal class AddToPortfolioBSContentUMFactory( portfolioUIData: PortfolioUIData, selectedWallet: UserWallet?, alreadyAddedNetworks: Set?, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return (currentState ?: TangemBottomSheetConfig.Empty).copy( isShown = portfolioUIData.portfolioBSVisibilityModel.addToPortfolioBSVisibility, @@ -110,7 +109,7 @@ internal class AddToPortfolioBSContentUMFactory( } private fun UserWallet.toSelectedUserWalletItemUM( - artwork: ArtworkModel? = null, + artwork: UserWalletItemUM.ImageState? = null, portfolioData: PortfolioData, balance: TotalFiatBalance?, ): UserWalletItemUM { @@ -128,7 +127,7 @@ internal class AddToPortfolioBSContentUMFactory( isShow: Boolean, portfolioData: PortfolioData, selectedWalletId: UserWalletId, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { return TangemBottomSheetConfig( isShown = isShow, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 60673724c0..5b6bd84ca2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -21,7 +22,6 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency @@ -31,7 +31,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.transaction.usecase.GetEnsNameUseCase -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.markets.impl.R @@ -42,13 +41,14 @@ import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject @@ -66,21 +66,17 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val getCardImageUseCase: GetCardImageUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, private val getEnsNameUseCase: GetEnsNameUseCase, + private val userWalletImageFetcher: UserWalletImageFetcher, ) : Model() { val state: StateFlow get() = _state private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - private val loadedArtworks: HashMap = hashMapOf() - private val artworksState: MutableStateFlow> = MutableStateFlow(hashMapOf()) - private val loadArtworksMutex = Mutex() - private val params = paramsContainer.require() private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( token = params.token, @@ -196,38 +192,33 @@ internal class MarketsPortfolioModel @Inject constructor( private fun subscribeOnStateUpdates() { combine( - flow = loadPortfolioData(params.token.id), + flow = loadPortfolioDataWithArtworks(params.token.id), flow2 = getPortfolioUIDataFlow(), - flow3 = artworksState, - transform = factory::create, + transform = { pair, portfolioUIData -> + val (portfolioData, artworks) = pair + factory.create(portfolioData, portfolioUIData, artworks) + }, ) .onEach { _state.value = it } .launchIn(modelScope) } - private fun loadPortfolioData(currencyRawId: CryptoCurrency.RawID): Flow { - portfolioDataLoader.load(currencyRawId).onEach { - loadArtworks(it.walletsWithCurrencies.keys.toList()) - }.also { return it } - } + private fun loadPortfolioDataWithArtworks( + currencyRawId: CryptoCurrency.RawID, + ): Flow>> { + val wallets = Channel>() + val portfolioFlow = portfolioDataLoader + .load(currencyRawId) + .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - private fun loadArtworks(wallets: List) { - modelScope.launch { - loadArtworksMutex.withLock { - wallets.filterIsInstance().forEach { wallet -> - if (!loadedArtworks.containsKey(wallet.walletId)) { - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - ) - loadedArtworks[wallet.walletId] = artwork - artworksState.emit(loadedArtworks) - } - } - } - } + val artworksFlow = wallets.receiveAsFlow() + .distinctUntilChanged() + .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } + + return combine( + flow = portfolioFlow, + flow2 = artworksFlow, + ) { portfolioData, artworks -> portfolioData to artworks } } private fun getPortfolioUIDataFlow(): Flow { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt index 5c5bb59b96..44fd810a9a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt @@ -1,8 +1,8 @@ package com.tangem.features.markets.portfolio.impl.model +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -36,7 +36,7 @@ internal class MyPortfolioUMFactory( fun create( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): MyPortfolioUM { val addToPortfolioData = portfolioUIData.addToPortfolioData @@ -89,7 +89,7 @@ internal class MyPortfolioUMFactory( private fun createAddToPortfolioBSConfig( portfolioData: PortfolioData, portfolioUIData: PortfolioUIData, - artworks: HashMap, + artworks: Map, ): TangemBottomSheetConfig { val selectedWallet = portfolioData.walletsWithCurrencies.keys .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 3f34abd5a1..4ab6bc0a40 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.features.onboardingV2.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.hotWallet.api) + implementation(projects.features.wallet.api) /* Project - Core */ implementation(projects.core.decompose) @@ -69,4 +70,5 @@ dependencies { /** Tangem libraries */ implementation(tangemDeps.hot.core) + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 69bf4cfb75..e716a35761 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState import com.tangem.core.analytics.DummyAnalyticsEventHandler import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.ui.components.block.model.BlockUM @@ -13,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.ui.WalletSettingsScreen @@ -34,14 +36,11 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { wallets = null, backedUp = false, ), - userWalletName = "My Wallet", isReferralAvailable = true, isLinkMoreCardsAvailable = true, - isRenameWalletAvailable = false, isNFTFeatureEnabled = true, isNFTEnabled = true, onCheckedNFTChange = {}, - renameWallet = {}, forgetWallet = {}, onLinkMoreCardsClick = {}, onReferralClick = {}, @@ -56,6 +55,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { onUpgradeWalletClick = {}, onDismissUpgradeWalletClick = {}, accountsUM = previewAccounts(), + cardItem = previewCardBlock(), ), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = {}, @@ -90,6 +90,15 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { ).let(::add) } + private fun previewCardBlock() = WalletSettingsItemUM.CardBlock( + id = "wallet_name", + title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), + text = stringReference("Wallet Name"), + isEnabled = true, + onClick = { }, + imageState = ImageState.MobileWallet, + ) + @Composable override fun Content(modifier: Modifier) { WalletSettingsScreen( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index 85a6bcb784..d2cb70fe36 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -2,6 +2,7 @@ package com.tangem.feature.walletsettings.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.account.CryptoPortfolioIconUM +import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -24,11 +25,12 @@ internal sealed class WalletSettingsItemUM { val onCheckedChange: (Boolean) -> Unit, ) : WalletSettingsItemUM() - data class WithText( + data class CardBlock( override val id: String, val title: TextReference, val text: TextReference, val isEnabled: Boolean, + val imageState: ImageState, val onClick: () -> Unit, ) : WalletSettingsItemUM() diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 1a5e3ee067..66ddf7f18f 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -1,6 +1,7 @@ package com.tangem.feature.walletsettings.model import android.os.Build +import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -44,6 +45,7 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList @@ -66,7 +68,7 @@ internal class WalletSettingsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, - private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + walletCardItemDelegateFactory: WalletCardItemDelegate.Factory, private val isDemoCardUseCase: IsDemoCardUseCase, getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, private val enableWalletNFTUseCase: EnableWalletNFTUseCase, @@ -85,6 +87,7 @@ internal class WalletSettingsModel @Inject constructor( val params: WalletSettingsComponent.Params = paramsContainer.require() val dialogNavigation = SlotNavigation() val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val walletCardItemDelegate = walletCardItemDelegateFactory.create(dialogNavigation) val state: MutableStateFlow = MutableStateFlow( value = WalletSettingsUM( @@ -117,14 +120,12 @@ internal class WalletSettingsModel @Inject constructor( } init { - combine( - getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), + fun combineUI(wallet: UserWallet) = combine( getWalletNFTEnabledUseCase.invoke(params.userWalletId), getWalletNotificationsEnabledUseCase(params.userWalletId), isUpgradeWalletNotificationEnabledUseCase(params.userWalletId), - ) { maybeWallet, nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled -> - val wallet = maybeWallet.getOrNull() ?: return@combine - val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + walletCardItemDelegate.cardItemFlow(wallet), + ) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem -> val isWalletBackedUp = when (wallet) { is UserWallet.Hot -> wallet.backedUp is UserWallet.Cold -> true @@ -135,8 +136,7 @@ internal class WalletSettingsModel @Inject constructor( value.copy( items = buildItems( userWallet = wallet, - dialogNavigation = dialogNavigation, - isRenameWalletAvailable = isRenameWalletAvailable, + cardItem = cardItem, isNFTEnabled = nftEnabled, isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = isNeedShowNotifications, @@ -147,6 +147,10 @@ internal class WalletSettingsModel @Inject constructor( ) } } + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .filterIsInstance>() + .flatMapLatest { combineUI(it.value) } .launchIn(modelScope) } @@ -162,8 +166,7 @@ internal class WalletSettingsModel @Inject constructor( private fun buildItems( userWallet: UserWallet, - dialogNavigation: SlotNavigation, - isRenameWalletAvailable: Boolean, + cardItem: WalletSettingsItemUM.CardBlock, isNFTEnabled: Boolean, isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, @@ -176,7 +179,7 @@ internal class WalletSettingsModel @Inject constructor( } return itemsBuilder.buildItems( userWallet = userWallet, - userWalletName = userWallet.name, + cardItem = cardItem, isReferralAvailable = when (userWallet) { is UserWallet.Cold -> userWallet.cardTypesResolver.isTangemWallet() is UserWallet.Hot -> false @@ -186,8 +189,6 @@ internal class WalletSettingsModel @Inject constructor( is UserWallet.Hot -> false }, isManageTokensAvailable = isMultiCurrency, - isRenameWalletAvailable = isRenameWalletAvailable, - renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, isNFTFeatureEnabled = isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, @@ -226,15 +227,6 @@ internal class WalletSettingsModel @Inject constructor( ) } - private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { - val config = DialogConfig.RenameWallet( - userWalletId = userWallet.walletId, - currentName = userWallet.name, - ) - - dialogNavigation.activate(config) - } - private fun forgetWallet() = modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { Timber.e("Unable to delete wallet: $it") diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index b18c80f1f0..c0d5e0cd86 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.common.ui.account.AccountRow +import com.tangem.common.ui.userwallet.CardImage import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.TangemSwitch @@ -30,10 +31,13 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockItem +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -115,7 +119,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { modifier = itemModifier, model = item, ) - is WalletSettingsItemUM.WithText -> TextBlock( + is WalletSettingsItemUM.CardBlock -> CardBlock( modifier = itemModifier, model = item, ) @@ -180,30 +184,40 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier } @Composable -private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) { +private fun CardBlock(model: WalletSettingsItemUM.CardBlock, modifier: Modifier = Modifier) { BlockCard( modifier = modifier.fillMaxWidth(), enabled = model.isEnabled, onClick = model.onClick, ) { - Column( + Row( modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = model.title.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - Text( - text = model.text.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - overflow = TextOverflow.Ellipsis, + CardImage(model.imageState) + Column(modifier = Modifier.weight(1f)) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + SecondarySmallButton( + config = SmallButtonConfig( + enabled = model.isEnabled, + text = resourceReference(R.string.common_rename), + onClick = model.onClick, + ), ) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index d26dc7fd98..6d2bf36754 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,7 +9,6 @@ import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -30,12 +29,11 @@ internal class ItemsBuilder @Inject constructor( @Suppress("LongParameterList") fun buildItems( userWallet: UserWallet, - userWalletName: String, + cardItem: WalletSettingsItemUM.CardBlock, accountsUM: List, isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, isManageTokensAvailable: Boolean, - isRenameWalletAvailable: Boolean, isNFTFeatureEnabled: Boolean, isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit, @@ -45,7 +43,6 @@ internal class ItemsBuilder @Inject constructor( onCheckedNotificationsChanged: (Boolean) -> Unit, onNotificationsDescriptionClick: () -> Unit, forgetWallet: () -> Unit, - renameWallet: () -> Unit, onLinkMoreCardsClick: () -> Unit, onReferralClick: () -> Unit, onAccessCodeClick: () -> Unit, @@ -53,7 +50,7 @@ internal class ItemsBuilder @Inject constructor( onUpgradeWalletClick: () -> Unit, onDismissUpgradeWalletClick: () -> Unit, ): PersistentList = persistentListOf() - .add(buildNameItem(userWalletName, isRenameWalletAvailable, renameWallet)) + .add(cardItem) .addAll( buildUpgradeWalletItem( userWallet = userWallet, @@ -62,8 +59,8 @@ internal class ItemsBuilder @Inject constructor( onDismissUpgradeWalletClick = onDismissUpgradeWalletClick, ), ) - .addAll(accountsUM) .addAll(buildAccessCodeItem(userWallet, onAccessCodeClick)) + .addAll(accountsUM) .add( buildCardItem( userWallet = userWallet, @@ -121,15 +118,6 @@ internal class ItemsBuilder @Inject constructor( } } - private fun buildNameItem(walletName: String, isRenameWalletAvailable: Boolean, renameWallet: () -> Unit) = - WalletSettingsItemUM.WithText( - id = "wallet_name", - title = resourceReference(id = R.string.settings_wallet_name_title), - text = stringReference(walletName), - isEnabled = isRenameWalletAvailable, - onClick = renameWallet, - ) - private fun buildNFTItem(isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit) = WalletSettingsItemUM.WithSwitch( id = "nft", diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt new file mode 100644 index 0000000000..322eaaac62 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.walletsettings.utils + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow + +internal class WalletCardItemDelegate @AssistedInject constructor( + private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, + private val walletImageFetcher: UserWalletImageFetcher, + @Assisted private val dialogNavigation: SlotNavigation, +) { + + fun cardItemFlow(wallet: UserWallet): Flow = combine( + flow = walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL), + flow2 = flow { emit(getShouldSaveUserWalletsSyncUseCase()) }, + transform = { imageState, isRenameAvailable -> + val walletName = wallet.name + WalletSettingsItemUM.CardBlock( + id = "wallet_name", + title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), + text = stringReference(walletName), + isEnabled = isRenameAvailable, + onClick = { openRenameWalletDialog(wallet) }, + imageState = imageState, + ) + }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + dialogNavigation.activate(config) + } + + @AssistedFactory + interface Factory { + fun create(dialogNavigation: SlotNavigation): WalletCardItemDelegate + } +} \ No newline at end of file diff --git a/features/wallet/api/build.gradle.kts b/features/wallet/api/build.gradle.kts index 3b1f5eeea2..4c23e065f7 100644 --- a/features/wallet/api/build.gradle.kts +++ b/features/wallet/api/build.gradle.kts @@ -15,6 +15,9 @@ dependencies { /** Project - Domain */ implementation(projects.domain.models) + /** Tangem libraries */ + implementation(tangemDeps.card.core) + /** Core */ implementation(projects.core.ui) implementation(projects.core.decompose) diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt new file mode 100644 index 0000000000..42738f4e11 --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletImageFetcher.kt @@ -0,0 +1,20 @@ +package com.tangem.features.wallet.utils + +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.Flow + +interface UserWalletImageFetcher { + + fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow + fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow + fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow + + fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt index 74a02c34b7..1a925f467e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureModule.kt @@ -4,8 +4,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.feature.wallet.DefaultWalletEntryComponent import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel import com.tangem.feature.wallet.child.wallet.model.WalletModel +import com.tangem.feature.wallet.utils.DefaultUserWalletImageFetcher import com.tangem.feature.wallet.utils.DefaultUserWalletsFetcher import com.tangem.features.wallet.WalletEntryComponent +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import dagger.Binds import dagger.Module @@ -13,6 +15,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -24,6 +27,10 @@ internal interface WalletFeatureModule { @Binds fun bindUserWalletsFetcher(impl: DefaultUserWalletsFetcher.Factory): UserWalletsFetcher.Factory + @Binds + @Singleton + fun bindUserWalletImageFetcher(impl: DefaultUserWalletImageFetcher): UserWalletImageFetcher + @Binds @IntoMap @ClassKey(WalletModel::class) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt new file mode 100644 index 0000000000..f6f11319dd --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletImageFetcher.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.utils + +import arrow.core.Either +import com.tangem.common.ui.userwallet.converter.ArtworkUMConverter +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.wallet.utils.UserWalletImageFetcher +import com.tangem.operations.attestation.ArtworkSize +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +class DefaultUserWalletImageFetcher @Inject constructor( + private val getCardImageUseCase: GetCardImageUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val artworkUMConverter: ArtworkUMConverter, +) : UserWalletImageFetcher { + + private val smallCache = MutableStateFlow(mapOf()) + private val largeCache = MutableStateFlow(mapOf()) + + override fun walletImage(wallet: UserWallet, size: ArtworkSize): Flow = when (wallet) { + is UserWallet.Cold -> walletImage(wallet.scanResponse.card, size) + is UserWallet.Hot -> flowOf(UserWalletItemUM.ImageState.MobileWallet) + } + + override fun walletsImage( + wallets: Collection, + size: ArtworkSize, + ): Flow> = wallets + .map { userWallet -> walletImage(userWallet, size).map { imageState -> userWallet.walletId to imageState } } + .merge() + .runningFold(mapOf()) { map, newState -> map.plus(newState) } + .filter { it.size >= wallets.size } // prevent spam, waiting full map + .distinctUntilChanged() + + override fun walletImage(walletId: UserWalletId, size: ArtworkSize): Flow = flow { + val imagesFlow = getUserWalletUseCase.invokeFlow(walletId) + // emit Loading and wait wallet + .onEach { if (it.isLeft()) emit(UserWalletItemUM.ImageState.Loading) } + .filterIsInstance>() + .map { it.value } + .distinctUntilChanged() + .flatMapLatest { wallet -> walletImage(wallet, size) } + emitAll(imagesFlow) + }.distinctUntilChanged() + + override fun walletImage(cardDTO: CardDTO, size: ArtworkSize): Flow = + internalGetCardImage( + cardInfo = cardDTO, + size = size, + ).distinctUntilChanged() + + private fun internalGetCardImage(cardInfo: CardDTO, size: ArtworkSize): Flow = flow { + emit(cacheOrLoading(cardInfo.cardId, size)) + + val artwork = getCardImageUseCase.invoke( + cardId = cardInfo.cardId, + cardPublicKey = cardInfo.cardPublicKey, + size = size, + manufacturerName = cardInfo.manufacturer.name, + firmwareVersion = cardInfo.firmwareVersion.toSdkFirmwareVersion(), + ) + .let { artworkUMConverter.convert(it) } + .also { save(cardInfo.cardId, size, it) } + emit(UserWalletItemUM.ImageState.Image(artwork)) + } + + private fun cacheOrLoading(cardId: String, size: ArtworkSize): UserWalletItemUM.ImageState { + val artwork = when (size) { + ArtworkSize.LARGE -> largeCache.value[cardId] + ArtworkSize.SMALL -> smallCache.value[cardId] + } + return artwork + ?.let { UserWalletItemUM.ImageState.Image(artwork) } + ?: UserWalletItemUM.ImageState.Loading + } + + private fun save(cardId: String, size: ArtworkSize, artwork: ArtworkUM) { + when (size) { + ArtworkSize.LARGE -> largeCache.update { it.plus(cardId to artwork) } + ArtworkSize.SMALL -> smallCache.update { it.plus(cardId to artwork) } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index 7a3b9accba..6a310e5c31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -14,16 +14,15 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.toLce -import com.tangem.domain.models.ArtworkModel import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -45,11 +44,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @Assisted private val messageSender: UiMessageSender, @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, @Assisted("authMode") private val authMode: Boolean, - private val getCardImageUseCase: GetCardImageUseCase, + private val userWalletImageFetcher: UserWalletImageFetcher, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { - private var loadedArtworks: HashMap = hashMapOf() private val walletsFlow = if (onlyMultiCurrency) getWalletsUseCase().map { it.filter { it.isMultiCurrency } } else getWalletsUseCase() @@ -67,7 +65,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(), flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(), flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), - flow4 = loadArtworks(wallets), + flow4 = userWalletImageFetcher.walletsImage(wallets, ArtworkSize.SMALL), ) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks -> createUiModels( wallets = wallets, @@ -90,29 +88,12 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( } .flowOn(dispatchers.default) - private fun loadArtworks(wallets: List): Flow> { - return flow { - emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish - wallets.filterIsInstance().forEach { wallet -> - val artwork = getCardImageUseCase( - cardId = wallet.cardId, - manufacturerName = wallet.scanResponse.card.manufacturer.name, - firmwareVersion = wallet.scanResponse.card.firmwareVersion.toSdkFirmwareVersion(), - cardPublicKey = wallet.scanResponse.card.cardPublicKey, - size = ArtworkSize.SMALL, - ) - loadedArtworks[wallet.walletId] = artwork - emit(loadedArtworks) - } - } - } - private fun createUiModels( wallets: List, maybeAppCurrency: Either, maybeBalances: Lce>, balanceHidingSettings: BalanceHidingSettings, - artworks: HashMap, + artworks: Map, ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, From 710002e992ce3e3026e83315d8bbaa5035193a8f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Aug 2025 19:31:47 +0400 Subject: [PATCH 164/165] Updated on 2026-08-14 --- data/account/build.gradle.kts | 10 +- .../AccountConverterFactoryContainer.kt | 20 ++++ .../store/AccountsResponseStoreFactory.kt | 66 ++++++++++++++ .../store/AccountsResponseStoreFactoryTest.kt | 91 +++++++++++++++++++ .../createedit/AccountCreateEditModel.kt | 18 +++- 5 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 287f2b190f..90800c1228 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -25,18 +25,24 @@ dependencies { api(projects.domain.models) // endregion - // Project - Data + // region Project - Data implementation(projects.data.common) // endregion // region DI - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) // endregion + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + // region Other Dependencies implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) implementation(deps.timber) // endregion diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt new file mode 100644 index 0000000000..28cfd01b9e --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -0,0 +1,20 @@ +package com.tangem.data.account.converter + +import javax.inject.Inject + +/** + * Container for converter factories related to accounts. + * + * @property accountsListCF factory for creating an account list converter + * @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter + * @property cryptoPortfolioCF factory for creating a crypto portfolio converter + * + * @constructor Creates an instance of the container with injected factories. + * +[REDACTED_AUTHOR] + */ +internal class AccountConverterFactoryContainer @Inject constructor( + val accountsListCF: AccountListConverter.Factory, + val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, + val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, +) \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt new file mode 100644 index 0000000000..a8ffea4d37 --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/store/AccountsResponseStoreFactory.kt @@ -0,0 +1,66 @@ +package com.tangem.data.account.store + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +typealias AccountsResponseStore = DataStore + +/** + * Factory class for creating and managing instances of [AccountsResponseStore]. + * This class is responsible for creating a [DataStore] for each unique [UserWalletId]. + * + * @property context application context used to access the file system + * @property moshi moshi instance for JSON serialization and deserialization + * @property dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +internal class AccountsResponseStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + @OptIn(ExperimentalStdlibApi::class) + private val adapter by lazy { moshi.adapter() } + + private val createdDataStores = ConcurrentHashMap() + + /** + * Creates or retrieves an [AccountsResponseStore] for the given [UserWalletId]. + * + * @param userWalletId the unique identifier of the user's wallet + */ + fun create(userWalletId: UserWalletId): AccountsResponseStore { + return createdDataStores.computeIfAbsent(userWalletId) { + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer(defaultValue = null, adapter = adapter), + produceFile = { context.dataStoreFile(fileName = "wallet_accounts_${userWalletId.stringValue}") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + } + + @VisibleForTesting + fun getAllStores(): Map = createdDataStores.toMap() + + @VisibleForTesting + fun clearStores() { + createdDataStores.clear() + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt new file mode 100644 index 0000000000..6c39f7181c --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/store/AccountsResponseStoreFactoryTest.kt @@ -0,0 +1,91 @@ +package com.tangem.data.account.store + +import android.content.Context +import com.google.common.truth.Truth +import com.squareup.moshi.Moshi +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.mockk +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AccountsResponseStoreFactoryTest { + + private val context: Context = mockk() + private val moshi: Moshi = Moshi.Builder().build() + private val factory: AccountsResponseStoreFactory = AccountsResponseStoreFactory( + context = context, + moshi = moshi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @AfterEach + fun setup() { + clearMocks(context) + factory.clearStores() + } + + @Test + fun `creates new data store for unique userWalletId`() { + // Arrange + val userWalletId = UserWalletId("011") + val createdStore = factory.create(userWalletId = userWalletId) + + // Actual + val actual = factory.getAllStores() + + // Assert + Truth.assertThat(actual).containsExactly(userWalletId, createdStore) + } + + @Test + fun `reuses existing data store for same userWalletId`() { + val userWalletId = UserWalletId("011") + + // Arrange (first creation) + val firstStore = factory.create(userWalletId = userWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(userWalletId, firstStore) + + // Arrange (second creation) + val secondStore = factory.create(userWalletId = userWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + Truth.assertThat(actual2).containsExactly(userWalletId, secondStore) + Truth.assertThat(firstStore).isSameInstanceAs(secondStore) + } + + @Test + fun `creates separate data stores for different userWalletIds`() { + // Arrange (first creation) + val firstWalletId = UserWalletId("011") + val firstStore = factory.create(userWalletId = firstWalletId) + + // Act (first creation) + val actual1 = factory.getAllStores() + + // Assert (first creation) + Truth.assertThat(actual1).containsExactly(firstWalletId, firstStore) + + // Arrange (second creation) + val secondWalletId = UserWalletId("011") + val secondStore = factory.create(userWalletId = secondWalletId) + + // Act (second creation) + val actual2 = factory.getAllStores() + + // Assert (second creation) + val expected = mapOf(firstWalletId to firstStore, secondWalletId to secondStore) + Truth.assertThat(actual2).containsExactlyEntriesIn(expected) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 5c61ebc3f4..03934e051e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit +import com.tangem.common.ui.account.toDomain import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.common.ui.account.toDomain import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -170,10 +170,14 @@ internal class AccountCreateEditModel @Inject constructor( it.updateDerivationIndex(derivationIndex = derivationIndex.value) } } - .onLeft { + .onLeft { cause -> handleError( error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, - params = mapOf("userWalletId" to userWalletId.stringValue), + message = cause.toString(), + params = mapOf( + "userWalletId" to userWalletId.stringValue, + "cause" to cause.toString(), + ), ) return@launch @@ -181,8 +185,12 @@ internal class AccountCreateEditModel @Inject constructor( } } - private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { - val exception = IllegalStateException(error.toString()) + private fun handleError( + error: AccountFeatureError, + message: String? = null, + params: Map = mapOf(), + ) { + val exception = IllegalStateException("$error. Cause: $message") Timber.e(exception) From 8ee35d12f8b97ac2abd473c88cb22b1e4eb02428 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Aug 2025 09:26:25 +0300 Subject: [PATCH 165/165] Updated on 2026-08-14 --- .../common/ui/userwallet/UserWalletItem.kt | 82 +++++++++++++------ .../converter/UserWalletItemUMConverter.kt | 3 +- .../ui/userwallet/state/UserWalletItemUM.kt | 2 + 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 31c66358b4..ad44dcf7dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -139,33 +139,59 @@ private fun NameAndInfo( ) } } - Text( - text = " $DOT ", - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - ) - AnimatedContent( - targetState = balance, - label = "Balance content", - ) { balance -> - val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + BalanceContent(balance) + } + } +} - if (balanceValue == null) { - TextShimmer( - style = TangemTheme.typography.caption2, - text = "aaaaa", - ) - } else { +@Composable +private fun BalanceContent(balance: UserWalletItemUM.Balance, modifier: Modifier = Modifier) { + AnimatedContent( + modifier = modifier, + targetState = balance, + label = "Balance content", + ) { balance -> + when (balance) { + UserWalletItemUM.Balance.Locked -> { + Icon( + modifier = Modifier + .padding(start = 4.dp, bottom = 2.dp, top = 2.dp) + .size(12.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_lock_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + UserWalletItemUM.Balance.NotShowing -> { + /** No balance and no dot */ + } + else -> { + Row { Text( - text = balanceValue, - style = TangemTheme.typography.caption2.applyBladeBrush( - isEnabled = isFlickering, - textColor = TangemTheme.colors.text.tertiary, - ), + text = " $DOT ", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, maxLines = 1, ) + + val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance) + + if (balanceValue == null) { + TextShimmer( + style = TangemTheme.typography.caption2, + text = "aaaaa", + ) + } else { + Text( + text = balanceValue, + style = TangemTheme.typography.caption2.applyBladeBrush( + isEnabled = isFlickering, + textColor = TangemTheme.colors.text.tertiary, + ), + maxLines = 1, + ) + } } } } @@ -246,9 +272,8 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair DASH_SIGN to false is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false - is UserWalletItemUM.Balance.Loading -> null to false - is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering + else -> null to false } } @@ -392,6 +417,15 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider UserWalletItemUM.Balance.Hidden userWallet.isLocked -> UserWalletItemUM.Balance.Locked + authMode -> UserWalletItemUM.Balance.NotShowing + isBalanceHidden -> UserWalletItemUM.Balance.Hidden balance == null -> UserWalletItemUM.Balance.Loading else -> { when (balance) { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index c0abb2b485..b31b4d5ddb 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -28,6 +28,8 @@ data class UserWalletItemUM( data object Hidden : Balance() + data object NotShowing : Balance() + data object Locked : Balance() data object Failed : Balance()