From e18f70eb715ea21fc22d97c87e1b827d04c26a2a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 11:39:52 +0500 Subject: [PATCH 001/110] Updated on 2026-08-14 --- .../com/tangem/tap/routing/ProxyAppRouter.kt | 1 + .../amountScreen/ui/AmountFieldContainer.kt | 2 +- .../src/main/res/drawable/ic_convert_24.xml | 9 +++ .../subcomponents/SwapAmountUpdateTrigger.kt | 9 +++ .../amount/DefaultSwapAmountUpdateTrigger.kt | 28 ++++++++ .../v2/impl/amount/SwapAmountComponent.kt | 1 + .../impl/amount/SwapAmountComponentParams.kt | 5 +- .../swap/v2/impl/amount/SwapAmountRoute.kt | 8 +++ .../v2/impl/amount/di/SwapAmountModule.kt | 15 +++++ .../amount/model/SwapAmountClickIntents.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 23 ++++++- .../v2/impl/amount/ui/SwapAmountContent.kt | 67 ++++++++++--------- .../ui/preview/SwapAmountClickIntentsStub.kt | 2 + 13 files changed, 135 insertions(+), 36 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_convert_24.xml create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountRoute.kt diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index cdebe55b4a..492ca42a8b 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -75,6 +75,7 @@ internal class ProxyAppRouter( try { block() } catch (e: Throwable) { + Timber.e(e) onComplete(false) } } 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 7b4dfe4e57..fe714763b9 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 @@ -101,7 +101,7 @@ internal fun LazyListScope.amountFieldV2( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier .padding(top = 48.dp, bottom = 28.dp), ) { diff --git a/core/ui/src/main/res/drawable/ic_convert_24.xml b/core/ui/src/main/res/drawable/ic_convert_24.xml new file mode 100644 index 0000000000..3c1c407a28 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_convert_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt new file mode 100644 index 0000000000..651312d68f --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt @@ -0,0 +1,9 @@ +package com.tangem.features.swap.v2.api.subcomponents + +/** + * Trigger swap amount change from another component. + * Different from another triggers because it takes raw string instead of BigDecimal + */ +interface SwapAmountUpdateTrigger { + suspend fun triggerUpdateAmount(amountValue: String) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt new file mode 100644 index 0000000000..8be3a3c254 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt @@ -0,0 +1,28 @@ +package com.tangem.features.swap.v2.impl.amount + +import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Trigger amount change from another component. + * Different from another triggers because it takes raw string instead of BigDecimal + */ +interface SwapAmountUpdateListener { + val updateAmountTriggerFlow: Flow +} + +@Singleton +internal class DefaultSwapAmountUpdateTrigger @Inject constructor() : + SwapAmountUpdateTrigger, + SwapAmountUpdateListener { + + override val updateAmountTriggerFlow: Flow + field = MutableSharedFlow() + + override suspend fun triggerUpdateAmount(amountValue: String) { + updateAmountTriggerFlow.emit(amountValue) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt index aeb060452e..b987f350eb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -38,5 +38,6 @@ internal class SwapAmountComponent @AssistedInject constructor( interface ModelCallback : SwapNavigationModelCallback { fun onAmountResult(amountUM: SwapAmountUM) + fun onSeparatorClick(lastAmount: String) } } \ No newline at end of file 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 beb46d4941..0f09fecef4 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 @@ -1,11 +1,11 @@ package com.tangem.features.swap.v2.impl.amount +import com.tangem.core.ui.extensions.TextReference 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.wallets.models.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.swap.SwapRoute import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -27,8 +27,9 @@ internal sealed class SwapAmountComponentParams { override val swapDirection: SwapDirection, override val primaryCryptoCurrencyStatusFlow: StateFlow, override val secondaryCryptoCurrency: CryptoCurrency?, + val title: TextReference, val callback: SwapAmountComponent.ModelCallback, - val currentRoute: Flow, + val currentRoute: Flow, ) : SwapAmountComponentParams() data class AmountBlockParams( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountRoute.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountRoute.kt new file mode 100644 index 0000000000..721299ab4f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountRoute.kt @@ -0,0 +1,8 @@ +package com.tangem.features.swap.v2.impl.amount + +/** + * Common interface for routing to swap amount + */ +interface SwapAmountRoute { + val isEditMode: Boolean +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt index c80a8ee734..56c67b48db 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/di/SwapAmountModule.kt @@ -2,10 +2,14 @@ package com.tangem.features.swap.v2.impl.amount.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger +import com.tangem.features.swap.v2.impl.amount.DefaultSwapAmountUpdateTrigger +import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap @@ -17,4 +21,15 @@ internal interface SwapAmountModule { @IntoMap @ClassKey(SwapAmountModel::class) fun provideSwapAmountModel(impl: SwapAmountModel): Model +} + +@InstallIn(SingletonComponent::class) +@Module +internal interface SwapAmountModuleBinds { + + @Binds + fun provideSwapAmountUpdateTrigger(impl: DefaultSwapAmountUpdateTrigger): SwapAmountUpdateTrigger + + @Binds + fun provideSwapAmountUpdateListener(impl: DefaultSwapAmountUpdateTrigger): SwapAmountUpdateListener } \ 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 34cf98f664..7e5a767bb6 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 @@ -8,4 +8,5 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onExpandEditField(selectedAmountType: SwapAmountType) fun onInfoClick() fun onSelectTokenClick() + fun onSeparatorClick() } \ 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 0b561eda91..81c5fc43e2 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 @@ -8,6 +8,7 @@ import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter 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.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -32,6 +33,7 @@ import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChooseProviderConfig import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams +import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener 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 @@ -39,7 +41,6 @@ import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadySt import com.tangem.features.swap.v2.impl.amount.model.transformers.* import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener -import com.tangem.common.ui.navigationButtons.NavigationUM 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.utils.coroutines.CoroutineDispatcherProvider @@ -67,6 +68,7 @@ internal class SwapAmountModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val appRouter: AppRouter, private val swapAlertFactory: SwapAlertFactory, + private val swapAmountUpdateListener: SwapAmountUpdateListener, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -99,6 +101,7 @@ internal class SwapAmountModel @Inject constructor( } configAmountNavigation() subscribeOnCryptoCurrencyStatusFlow() + subscribeOnAmountUpdateTriggerUpdates() observeChooseSelectToken() // todo observe balance hiding flow } @@ -189,6 +192,14 @@ internal class SwapAmountModel @Inject constructor( ) } + override fun onSeparatorClick() { + // todo send with swap make swap types + val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data + val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return + + callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty()) + } + private fun subscribeOnCryptoCurrencyStatusFlow() { params.primaryCryptoCurrencyStatusFlow.onEach { primaryCurrencyStatus -> primaryCryptoCurrencyStatus = primaryCurrencyStatus @@ -210,6 +221,16 @@ internal class SwapAmountModel @Inject constructor( }.launchIn(modelScope) } + private fun subscribeOnAmountUpdateTriggerUpdates() { + swapAmountUpdateListener.updateAmountTriggerFlow + .onEach { + if (uiState.value is SwapAmountUM.Content) { + onAmountValueChange(it) + } + } + .launchIn(modelScope) + } + private fun observeChooseSelectToken() { swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow .onEach { currency -> 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 671ce82c48..212a0ca592 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 @@ -27,11 +27,9 @@ 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.SecondaryButton import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.conditionalCompose @@ -81,6 +79,7 @@ internal fun SwapAmountContent( }, ) SwapAmountBlockSeparator( + onClick = clickIntents::onSeparatorClick, modifier = Modifier .constrainAs(middleButtonRef) { top.linkTo(amountFromRef.bottom) @@ -93,7 +92,7 @@ internal fun SwapAmountContent( } @Composable -private fun SwapAmountBlockSeparator(modifier: Modifier = Modifier) { +private fun SwapAmountBlockSeparator(onClick: () -> Unit, modifier: Modifier = Modifier) { // todo add swap type like [Swap, SendViaSwap, SendIncognito] Row( verticalAlignment = Alignment.CenterVertically, @@ -102,13 +101,11 @@ private fun SwapAmountBlockSeparator(modifier: Modifier = Modifier) { .heightIn(max = 28.dp) .clip(RoundedCornerShape(32.dp)) .background(TangemTheme.colors.background.secondary) - .padding( - vertical = 6.dp, - horizontal = 12.dp, - ), + .clickable(onClick = onClick) + .padding(vertical = 6.dp, horizontal = 12.dp), ) { Text( - text = "Convert", + text = stringResourceSafe(R.string.common_convert), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, ) @@ -180,8 +177,7 @@ private fun SwapAmountEditBlock( Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier - .padding(vertical = 48.dp), + modifier = modifier.padding(top = 48.dp, bottom = 28.dp), ) { if (amountFieldUM.amountField !is AmountState.Data) { TextShimmer( @@ -263,24 +259,10 @@ private fun SwapAmountInfo( @Composable private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modifier = Modifier) { AnimatedContent( - targetState = amountFieldUM !is SwapAmountFieldUM.Content, + targetState = amountFieldUM is SwapAmountFieldUM.Content, modifier = modifier, ) { isContent -> - if (isContent) { - 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), - ) - } - } else { - val amountFieldUM = amountFieldUM as SwapAmountFieldUM.Content + if (isContent && amountFieldUM is SwapAmountFieldUM.Content) { Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { @@ -297,6 +279,19 @@ private fun SwapAmountInfoMain(amountFieldUM: SwapAmountFieldUM, modifier: Modif 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), + ) + } } } } @@ -323,8 +318,7 @@ private fun SwapAmountInfoExtra( SwapAmountInfoQuote( quoteUM = amountUM.selectedQuote, swapRateType = amountUM.swapRateType, - onSelectTokenClick = - onSelectTokenClick, + onSelectTokenClick = onSelectTokenClick, ) } } @@ -352,11 +346,20 @@ private fun SwapAmountInfoExtra( @Composable private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { - SecondaryButton( + Text( text = stringResourceSafe(R.string.send_max_amount), - size = TangemButtonSize.Small, - onClick = onMaxAmountClick, - modifier = Modifier.padding(end = 16.dp), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(end = 16.dp) + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = 12.dp, vertical = 4.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = onMaxAmountClick, + ), ) } 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 194e61c3d3..1c60ad739a 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 @@ -10,6 +10,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSelectTokenClick() {} + override fun onSeparatorClick() {} + override fun onAmountValueChange(value: String) {} override fun onAmountPasteTriggerDismiss() {} From 402167c31eba3514cd0a2b50795b0da9379543f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 12:22:00 +0500 Subject: [PATCH 002/110] Updated on 2026-08-14 --- .../send/v2/api/entity/FeeSelectorUM.kt | 4 +- .../DefaultFeeSelectorBlockComponent.kt | 2 + .../feeselector/model/FeeSelectorIntents.kt | 2 - .../v2/feeselector/model/FeeSelectorModel.kt | 15 ++-- .../model/transformers/FeeItemConverter.kt | 41 ++++++---- .../FeeSelectorCustomFieldConverter.kt | 81 ++++++++++--------- ...eeSelectorCustomValueChangedTransformer.kt | 38 +++++++++ .../FeeSelectorLoadedTransformer.kt | 10 ++- .../ui/FeeSelectorModalBottomSheet.kt | 8 +- .../bitcoin/BitcoinCustomFeeConverter.kt | 12 ++- .../ethereum/EthereumCustomFeeConverter.kt | 8 +- 11 files changed, 149 insertions(+), 72 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt 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 e71f997e18..15f8c51974 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 @@ -2,6 +2,7 @@ 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.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError @@ -17,6 +18,7 @@ sealed class FeeSelectorUM { data class Error(val error: GetFeeError) : FeeSelectorUM() data class Content( + val fees: TransactionFee, val feeItems: ImmutableList, val selectedFeeItem: FeeItem, val isFeeApproximate: Boolean, @@ -37,7 +39,7 @@ data class FeeFiatRateUM( sealed class FeeItem { abstract val fee: Fee - fun isSame(other: FeeItem): Boolean { + fun isSameClass(other: FeeItem): Boolean { return this::class == other::class } 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 28f80f7202..8e9135b92a 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 androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -189,6 +190,7 @@ private fun FeeSelectorBlockContent_Preview() { displayNonceInput = false, nonce = null, onNonceChange = {}, + fees = TransactionFee.Single(feeItem.fee), ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt index 96c3867911..bab25e894b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt @@ -5,13 +5,11 @@ import com.tangem.features.send.v2.api.entity.FeeItem internal interface FeeSelectorIntents { fun onFeeItemSelected(feeItem: FeeItem) fun onCustomFeeValueChange(index: Int, value: String) - fun onCustomFeeNextClick() fun onDoneClick() } internal class StubFeeSelectorIntents : FeeSelectorIntents { override fun onFeeItemSelected(feeItem: FeeItem) {} override fun onCustomFeeValueChange(index: Int, value: String) {} - override fun onCustomFeeNextClick() {} override fun onDoneClick() {} } \ No newline at end of file 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 11e7a9037a..d15ee2d65b 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 @@ -14,6 +14,7 @@ import com.tangem.features.send.v2.api.entity.FeeItem 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.transformers.FeeItemSelectedTransformer +import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorCustomValueChangedTransformer import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -89,11 +90,15 @@ internal class FeeSelectorModel @Inject constructor( } override fun onCustomFeeValueChange(index: Int, value: String) { - // TODO: [REDACTED_JIRA] - } - - override fun onCustomFeeNextClick() { - // TODO: [REDACTED_JIRA] + uiState.update( + FeeSelectorCustomValueChangedTransformer( + index = index, + value = value, + intents = this, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = params.cryptoCurrencyStatus, + ), + ) } override fun onDoneClick() { 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 a4657cef69..e8ef3fec18 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 @@ -4,8 +4,8 @@ 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.api.params.FeeSelectorParams 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 import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList @@ -17,7 +17,7 @@ internal class FeeItemConverter( private val feeSelectorIntents: FeeSelectorIntents, private val appCurrency: AppCurrency, cryptoCurrencyStatus: CryptoCurrencyStatus, -) : Converter> { +) : Converter> { private val customFeeFieldConverter = FeeSelectorCustomFieldConverter( feeSelectorIntents = feeSelectorIntents, @@ -26,7 +26,7 @@ internal class FeeItemConverter( normalFee = normalFee, ) - override fun convert(value: TransactionFee): ImmutableList { + override fun convert(value: Input): ImmutableList { val fees = mutableListOf() when (suggestedFeeState) { @@ -38,26 +38,33 @@ internal class FeeItemConverter( ), ) } - when (value) { + when (value.transactionFee) { is TransactionFee.Choosable -> { - fees.add(FeeItem.Slow(fee = value.minimum)) - fees.add(FeeItem.Market(fee = value.normal)) - fees.add(FeeItem.Fast(fee = value.priority)) + fees.add(FeeItem.Slow(fee = value.transactionFee.minimum)) + fees.add(FeeItem.Market(fee = value.transactionFee.normal)) + fees.add(FeeItem.Fast(fee = value.transactionFee.priority)) } is TransactionFee.Single -> { - fees.add(FeeItem.Market(fee = value.normal)) + fees.add(FeeItem.Market(fee = value.transactionFee.normal)) } } - val customFeeFields = customFeeFieldConverter.convert(normalFee) - if (customFeeFields.isNotEmpty()) { - fees.add( - FeeItem.Custom( - fee = customFeeFieldConverter.convertBack(customFeeFields), - customValues = customFeeFields, - ), - ) - } + + val customFee = value.customFee ?: constructCustomFee() + customFee?.let(fees::add) return fees.toImmutableList() } + + private fun constructCustomFee(): FeeItem.Custom? { + val customFeeFields = customFeeFieldConverter.convert(normalFee) + + if (customFeeFields.isEmpty()) return null + + return FeeItem.Custom( + fee = customFeeFieldConverter.convertBack(customFeeFields), + customValues = customFeeFields, + ) + } + + data class Input(val transactionFee: TransactionFee, val customFee: FeeItem.Custom?) } \ No newline at end of file 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 d779c46037..7a2265d453 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 @@ -5,12 +5,12 @@ 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.features.send.v2.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents 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.feeselector.model.FeeSelectorIntents -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -25,7 +25,7 @@ internal class FeeSelectorCustomFieldConverter( private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { EthereumCustomFeeConverter( onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange, - onNextClick = feeSelectorIntents::onCustomFeeNextClick, + onNextClick = null, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -34,7 +34,7 @@ internal class FeeSelectorCustomFieldConverter( private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { BitcoinCustomFeeConverter( onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange, - onNextClick = feeSelectorIntents::onCustomFeeNextClick, + onNextClick = null, appCurrency = appCurrency, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, ) @@ -77,38 +77,43 @@ internal class FeeSelectorCustomFieldConverter( } } - fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) = - when (val fee = feeSelectorState.fees.normal) { - is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( - feeValue = fee, - customValues = feeSelectorState.customValues, - index = index, - value = value, - ) - is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange( - customValues = feeSelectorState.customValues, - index = index, - value = value, - txSize = fee.txSize, - ) - is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange( - customValues = feeSelectorState.customValues, - index = index, - value = value, - ) - else -> feeSelectorState.customValues - } - - fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) { - is TransactionFee.Choosable -> feeSelectorState.fees.minimum - is TransactionFee.Single -> feeSelectorState.fees.normal - }.let { - when (it) { - is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( - minimumFee = it, - customValues = feeSelectorState.customValues, - ) - else -> feeSelectorState.customValues - } + fun onValueChange( + feeSelectorState: FeeSelectorUM.Content, + customValues: ImmutableList, + index: Int, + value: String, + ) = when (val fee = feeSelectorState.fees.normal) { + is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( + feeValue = fee, + customValues = customValues, + index = index, + value = value, + ) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange( + customValues = customValues, + index = index, + value = value, + txSize = fee.txSize, + ) + is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange( + customValues = customValues, + index = index, + value = value, + ) + else -> customValues } + + fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content, customValues: ImmutableList) = + when (val fees = feeSelectorState.fees) { + is TransactionFee.Choosable -> fees.minimum + is TransactionFee.Single -> fees.normal + }.let { + when (it) { + is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( + minimumFee = it, + customValues = customValues, + ) + else -> customValues + } + } } \ No newline at end of file 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 new file mode 100644 index 0000000000..d1c3b9c9e0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorCustomValueChangedTransformer.kt @@ -0,0 +1,38 @@ +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.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal class FeeSelectorCustomValueChangedTransformer( + private val index: Int, + private val value: String, + private val intents: FeeSelectorIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { + val state = prevState as? FeeSelectorUM.Content ?: return prevState + val customFee = state.feeItems.filterIsInstance().firstOrNull() ?: return prevState + val customFeeConverter = FeeSelectorCustomFieldConverter( + feeSelectorIntents = intents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + normalFee = state.selectedFeeItem.fee, + ) + val updatedCustomValues = customFeeConverter.onValueChange(state, customFee.customValues, index, value) + val newCustomFee = customFee.copy( + fee = customFeeConverter.convertBack(updatedCustomValues), + customValues = updatedCustomValues, + ) + return state.copy( + feeItems = state.feeItems.map { if (it is FeeItem.Custom) newCustomFee else it }.toImmutableList(), + selectedFeeItem = newCustomFee, + ) + } +} \ No newline at end of file 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 a4aee4e3c2..7d6b828036 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 @@ -30,16 +30,22 @@ internal class FeeSelectorLoadedTransformer( ) override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { - val feeItems: ImmutableList = feeItemsConverter.convert(fees) + val prevCustomFee = if (prevState is FeeSelectorUM.Content) { + prevState.feeItems.find { it is FeeItem.Custom } as? FeeItem.Custom + } else { + null + } + val feeItems: ImmutableList = feeItemsConverter.convert(FeeItemConverter.Input(fees, prevCustomFee)) val selectedFee = when (prevState) { - is FeeSelectorUM.Content -> feeItems.first { it.isSame(prevState.selectedFeeItem) } + is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) } is FeeSelectorUM.Error, FeeSelectorUM.Loading, -> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market } } return FeeSelectorUM.Content( + fees = fees, feeItems = feeItems, selectedFeeItem = selectedFee, isFeeApproximate = isFeeApproximate, 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 c2decb1bdb..fc27111f09 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 @@ -31,6 +31,7 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.atoms.text.EllipsisText @@ -111,7 +112,7 @@ private fun FeeSelectorItems( Column(modifier = modifier) { val feeFiatRateUM = state.feeFiatRateUM state.feeItems.fastForEachIndexed { index, item -> - val isSelected = item.isSame(state.selectedFeeItem) + val isSelected = item.isSameClass(state.selectedFeeItem) val lastItem = index == state.feeItems.size - 1 val iconTint by animateColorAsState( targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary, @@ -255,6 +256,7 @@ private fun FeeSelectorItems( isSelected = isSelected, iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, + onValueChange = feeSelectorIntents::onCustomFeeValueChange, displayNonceInput = state.displayNonceInput, nonce = state.nonce, onNonceChange = state.onNonceChange, @@ -271,6 +273,7 @@ private fun CustomFeeBlock( isSelected: Boolean, iconBackgroundColor: Color, iconTint: Color, + onValueChange: (Int, String) -> Unit, displayNonceInput: Boolean, nonce: BigInteger?, onNonceChange: (String) -> Unit, @@ -305,7 +308,7 @@ private fun CustomFeeBlock( ) { ExpandedCustomFeeItems( customFeeFields = customFee.customValues, - onValueChange = { _, _ -> }, + onValueChange = onValueChange, displayNonceInput = displayNonceInput, nonce = nonce, onNonceChange = onNonceChange, @@ -503,6 +506,7 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< displayNonceInput = true, onNonceChange = {}, nonce = null, + fees = TransactionFee.Single(customFeeItem.fee), ), ), ) 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 3d65f6b2b1..efb7fee254 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 @@ -11,10 +11,10 @@ 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.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.CustomFeeConverter -import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.lib.crypto.BlockchainUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +24,7 @@ import java.math.RoundingMode internal class BitcoinCustomFeeConverter( private val onCustomFeeValueChange: (Int, String) -> Unit, - private val onNextClick: () -> Unit, + private val onNextClick: (() -> Unit)?, private val appCurrency: AppCurrency, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : CustomFeeConverter { @@ -78,7 +78,13 @@ internal class BitcoinCustomFeeConverter( }, keyboardType = KeyboardType.Companion.Number, ), - keyboardActions = KeyboardActions(onDone = { onNextClick() }), + keyboardActions = KeyboardActions( + onDone = if (onNextClick != null) { + { onNextClick() } + } else { + null + }, + ), ), ) } else { 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 235638c7ff..b16dd562a8 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 @@ -18,7 +18,7 @@ import kotlinx.collections.immutable.toImmutableList internal class EthereumCustomFeeConverter( private val onCustomFeeValueChange: (Int, String) -> Unit, - private val onNextClick: () -> Unit, + private val onNextClick: (() -> Unit)?, private val appCurrency: AppCurrency, feeCryptoCurrencyStatus: CryptoCurrencyStatus, ) : BaseEthereumCustomFeeConverter { @@ -112,7 +112,11 @@ internal class EthereumCustomFeeConverter( keyboardType = KeyboardType.Number, ), keyboardActions = KeyboardActions( - onDone = { onNextClick() }, + onDone = if (onNextClick != null) { + { onNextClick() } + } else { + null + }, ), ) } From d602f2af48d86e9be3f759b8d6f2b16ca5b2e343 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 13:04:30 +0500 Subject: [PATCH 003/110] Updated on 2026-08-14 --- .../api/choosetoken/SwapChooseTokenNetworkTrigger.kt | 8 ++++++++ .../swap/v2/impl/amount/model/SwapAmountModel.kt | 2 +- .../chooseprovider/model/SwapChooseProviderModel.kt | 1 + .../v2/impl/choosetoken/di/SwapChooseTokenModule.kt | 2 +- .../DefaultSwapChooseTokenNetworkTrigger.kt | 12 +++--------- .../fromSupported/model/SwapChooseTokenFactory.kt | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt index 5940a10981..e001c83b8b 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/choosetoken/SwapChooseTokenNetworkTrigger.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.api.choosetoken import com.tangem.domain.models.currency.CryptoCurrency +import kotlinx.coroutines.flow.SharedFlow /** * Triggers swap token selection @@ -8,4 +9,11 @@ import com.tangem.domain.models.currency.CryptoCurrency interface SwapChooseTokenNetworkTrigger { suspend fun trigger(cryptoCurrency: CryptoCurrency) +} + +/** + * Listens to swap token selection + */ +interface SwapChooseTokenNetworkListener { + val swapChooseTokenNetworkResultFlow: SharedFlow } \ 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 81c5fc43e2..4debca50a0 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 @@ -30,6 +30,7 @@ import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent.SwapChooseProviderConfig import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams @@ -40,7 +41,6 @@ import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadyStateConverter import com.tangem.features.swap.v2.impl.amount.model.transformers.* import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent -import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener 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.utils.coroutines.CoroutineDispatcherProvider 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 52fe541a8d..5b9ce17796 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 @@ -33,6 +33,7 @@ internal class SwapChooseProviderModel @Inject constructor( fun onProviderClick(quoteUM: SwapQuoteUM) { params.callback.onProviderResult(quoteUM) + params.onDismiss() } private fun getInitialState(): SwapChooseProviderBottomSheetContent { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt index 187db962f7..690ac98f54 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/di/SwapChooseTokenModule.kt @@ -3,10 +3,10 @@ package com.tangem.features.swap.v2.impl.choosetoken.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkComponent +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkComponent import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.DefaultSwapChooseTokenNetworkTrigger -import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.impl.choosetoken.fromSupported.model.SwapChooseTokenNetworkModel import dagger.Binds import dagger.Module diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt index 887637781f..8ff862deb8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/DefaultSwapChooseTokenNetworkTrigger.kt @@ -1,19 +1,13 @@ package com.tangem.features.swap.v2.impl.choosetoken.fromSupported import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkListener import com.tangem.features.swap.v2.api.choosetoken.SwapChooseTokenNetworkTrigger import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import javax.inject.Inject import javax.inject.Singleton -/** - * Listens to swap token selection - */ -interface SwapChooseTokenNetworkListener { - val swapChooseTokenNetworkResultFlow: SharedFlow -} - @Singleton internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() : SwapChooseTokenNetworkTrigger, @@ -22,7 +16,7 @@ internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() : override val swapChooseTokenNetworkResultFlow: SharedFlow field = MutableSharedFlow() - override suspend fun trigger(result: CryptoCurrency) { - swapChooseTokenNetworkResultFlow.emit(result) + override suspend fun trigger(cryptoCurrency: CryptoCurrency) { + swapChooseTokenNetworkResultFlow.emit(cryptoCurrency) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt index b7e4427eb3..7b7b1752ce 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenFactory.kt @@ -10,7 +10,7 @@ internal object SwapChooseTokenFactory { fun getErrorMessage(tokenName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { - icon(R.drawable.img_attention_20) { + icon(R.drawable.ic_alert_triangle_20) { type = MessageBottomSheetUMV2.Icon.Type.Attention backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint } From 4104aa82f896cce0d0e08788424c274d5ad038f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Jul 2025 16:49:46 +0400 Subject: [PATCH 004/110] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 23 + .../com/tangem/common/test/utils/TruthExt.kt | 1 + ...tMultiWalletCryptoCurrenciesFetcherTest.kt | 4 +- .../tokens/wallet/WalletBalanceFetcher.kt | 163 ++++++ .../tokens/wallet/WalletBalanceFetcherTest.kt | 535 ++++++++++++++++++ 5 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.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 68ed536c3d..359aca0f8d 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 @@ -22,6 +22,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.wallet.WalletBalanceFetcher import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -432,4 +433,26 @@ internal object TokensDomainModule { fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { return GetCryptoCurrenciesUseCase(currenciesRepository) } + + @Provides + @Singleton + fun provideWalletBalanceFetcher( + currenciesRepository: CurrenciesRepository, + multiWalletCryptoCurrenciesFetcher: MultiWalletCryptoCurrenciesFetcher, + multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + dispatchers: CoroutineDispatcherProvider, + ): WalletBalanceFetcher { + return WalletBalanceFetcher( + currenciesRepository = currenciesRepository, + multiWalletCryptoCurrenciesFetcher = multiWalletCryptoCurrenciesFetcher, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, + dispatchers = dispatchers, + ) + } } \ No newline at end of file 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 1585424ba5..c8a88ac0ff 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 @@ -10,5 +10,6 @@ fun assertEither(actual: Either, expected: Either { + + /** Additional constructor without internal dependencies */ + constructor( + currenciesRepository: CurrenciesRepository, + multiWalletCryptoCurrenciesFetcher: MultiWalletCryptoCurrenciesFetcher, + multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, + dispatchers: CoroutineDispatcherProvider, + ) : this( + currenciesRepository = currenciesRepository, + multiWalletBalanceFetcher = MultiWalletBalanceFetcher( + multiWalletCryptoCurrenciesFetcher = multiWalletCryptoCurrenciesFetcher, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + ), + singleWalletWithTokenBalanceFetcher = SingleWalletWithTokenBalanceFetcher( + currenciesRepository = currenciesRepository, + ), + singleWalletBalanceFetcher = SingleWalletBalanceFetcher(currenciesRepository = currenciesRepository), + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, + dispatchers = dispatchers, + ) + + override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { + val userWalletId = params.userWalletId + val cardTypesResolver = currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + + val fetcher = when { + cardTypesResolver.isMultiwalletAllowed() -> multiWalletBalanceFetcher + cardTypesResolver.isSingleWalletWithToken() -> singleWalletWithTokenBalanceFetcher + cardTypesResolver.isSingleWallet() -> singleWalletBalanceFetcher + else -> error("Unknown type of wallet: $userWalletId") + } + + val currencies = fetcher.getCryptoCurrencies(userWalletId = userWalletId).ifEmpty { + error("UserWallet doesn't contain crypto-currencies: $userWalletId") + } + + fetcher.fetch(userWalletId = userWalletId, currencies = currencies) + } + + private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set) { + coroutineScope { + val results = fetchingSources.map { source -> + async { + val maybeResult = when (source) { + FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies) + FetchingSource.QUOTE -> fetchQuotes(currencies = currencies) + FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies) + } + + source to maybeResult + } + } + .awaitAll() + + val errors = results.mapNotNull { (source, maybeResult) -> + val error = maybeResult.leftOrNull() ?: return@mapNotNull null + + source to error + } + + check(errors.isEmpty()) { + val message = "Failed to fetch next sources for $userWalletId:\n" + + errors.joinToString(separator = "\n") { "${it.first.name} – ${it.second}" } + + Timber.e(message) + + message + } + } + } + + private suspend fun fetchNetworks( + userWalletId: UserWalletId, + currencies: Set, + ): Either { + return multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ), + ) + } + + private suspend fun fetchQuotes(currencies: Set): Either { + return multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ), + ) + } + + 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 }, + ), + ) + } + + /** + * Params of [WalletBalanceFetcher] + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a31d8a1fb9 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -0,0 +1,535 @@ +package com.tangem.domain.tokens.wallet + +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.domain.common.CardTypesResolver +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +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.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +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) +internal class WalletBalanceFetcherTest { + + private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() + + private val currenciesRepository: CurrenciesRepository = mockk() + private val multiWalletBalanceFetcher: MultiWalletBalanceFetcher = mockk() + private val singleWalletWithTokenBalanceFetcher: SingleWalletWithTokenBalanceFetcher = mockk() + private val singleWalletBalanceFetcher: SingleWalletBalanceFetcher = mockk() + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher = mockk() + + private val fetcher = WalletBalanceFetcher( + currenciesRepository = currenciesRepository, + multiWalletBalanceFetcher = multiWalletBalanceFetcher, + singleWalletWithTokenBalanceFetcher = singleWalletWithTokenBalanceFetcher, + singleWalletBalanceFetcher = singleWalletBalanceFetcher, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @BeforeEach + fun resetMocks() { + clearMocks( + currenciesRepository, + multiWalletBalanceFetcher, + singleWalletWithTokenBalanceFetcher, + singleWalletBalanceFetcher, + multiNetworkStatusFetcher, + multiQuoteStatusFetcher, + multiYieldBalanceFetcher, + ) + } + + @Test + fun `fetch failure if getCardTypesResolver THROWS EXCEPTION`() = runTest { + // Arrange + val exception = IllegalStateException("Error") + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } throws exception + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = exception.left() + assertEither(actual, expected) + + verifyOrder { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } + + coVerify(inverse = true) { + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if getCardTypesResolver cannot resolve wallet type`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns false + every { isSingleWalletWithToken() } returns false + every { isSingleWallet() } returns false + } + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException("Unknown type of wallet: $userWalletId").left() + assertEither(actual, expected) + + verifyOrder { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } + + coVerify(inverse = true) { + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if getCryptoCurrencies THROWS EXCEPTION`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val exception = IllegalStateException("Error") + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } throws exception + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = exception.left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + } + + 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 getCryptoCurrencies RETURNS EMPTY SET`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns emptySet() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException("UserWallet doesn't contain crypto-currencies: $userWalletId").left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + } + + 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 fetchNetworks RETURNS LEFT`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val networkStatusFetcherParams = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ) + + val exception = IllegalStateException("Error") + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK) + coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException( + "Failed to fetch next sources for UserWalletId(011...011):\n" + + "NETWORK – java.lang.IllegalStateException: Error", + ).left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + multiNetworkStatusFetcher(params = networkStatusFetcherParams) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiQuoteStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if fetchQuotes RETURNS LEFT`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val quoteStatusFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ) + + val exception = IllegalStateException("Error") + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(QUOTE) + coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException( + "Failed to fetch next sources for UserWalletId(011...011):\n" + + "QUOTE – java.lang.IllegalStateException: Error", + ).left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch failure if fetchStaking RETURNS LEFT`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ) + + val exception = IllegalStateException("Error") + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(STAKING) + coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException( + "Failed to fetch next sources for UserWalletId(011...011):\n" + + "STAKING – java.lang.IllegalStateException: Error", + ).left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiNetworkStatusFetcher(params = any()) + multiQuoteStatusFetcher(params = any()) + } + } + + @Test + fun `fetch failure if all fetching sources RETURNS LEFT`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + val networkStatusFetcherParams = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ) + + val quoteStatusFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ) + + val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ) + + val exception = IllegalStateException("Error") + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) + coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() + coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() + coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns exception.left() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = IllegalStateException( + "Failed to fetch next sources for UserWalletId(011...011):\n" + + "NETWORK – java.lang.IllegalStateException: Error\n" + + "QUOTE – java.lang.IllegalStateException: Error\n" + + "STAKING – java.lang.IllegalStateException: Error", + ).left() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + multiNetworkStatusFetcher(params = networkStatusFetcherParams) + multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + } + } + + @Test + fun `fetch successfully for multi-currency wallet`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + val networkStatusFetcherParams = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ) + + val quoteStatusFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ) + + val yieldBalanceFetcherParams = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { multiWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE, STAKING) + coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() + coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() + coEvery { multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) } returns Unit.right() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + multiWalletBalanceFetcher.fetchingSources + multiNetworkStatusFetcher(params = networkStatusFetcherParams) + multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + multiYieldBalanceFetcher(params = yieldBalanceFetcherParams) + } + + coVerify(inverse = true) { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + } + } + + @Test + fun `fetch successfully for single-currency with token wallet`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns false + every { isSingleWalletWithToken() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + val networkStatusFetcherParams = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ) + + val quoteStatusFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + } returns currencies + every { singleWalletWithTokenBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE) + coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() + coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + singleWalletWithTokenBalanceFetcher.fetchingSources + multiNetworkStatusFetcher(params = networkStatusFetcherParams) + multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + } + + coVerify(inverse = true) { + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + @Test + fun `fetch successfully for single-currency wallet`() = runTest { + // Arrange + val cardTypesResolver = mockk { + every { isMultiwalletAllowed() } returns false + every { isSingleWalletWithToken() } returns false + every { isSingleWallet() } returns true + } + + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + + val networkStatusFetcherParams = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::network), + ) + + val quoteStatusFetcherParams = MultiQuoteStatusFetcher.Params( + currenciesIds = currencies.mapNotNullTo(destination = hashSetOf(), transform = { it.id.rawCurrencyId }), + appCurrencyId = null, + ) + + every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver + coEvery { singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns currencies + every { singleWalletBalanceFetcher.fetchingSources } returns setOf(NETWORK, QUOTE) + coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns Unit.right() + coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() + + // Act + val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + + // Assert + val expected = Unit.right() + assertEither(actual, expected) + + coVerifyOrder { + currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) + singleWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) + singleWalletBalanceFetcher.fetchingSources + multiNetworkStatusFetcher(params = networkStatusFetcherParams) + multiQuoteStatusFetcher(params = quoteStatusFetcherParams) + } + + coVerify(inverse = true) { + multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + singleWalletWithTokenBalanceFetcher.getCryptoCurrencies(userWalletId = any()) + multiYieldBalanceFetcher(params = any()) + } + } + + private companion object { + + val userWalletId = UserWalletId("011") + } +} \ No newline at end of file From a1d92a54484e7a7fcb3fb3b5ecb7ff099f27bcbf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 15:29:48 +0700 Subject: [PATCH 005/110] 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 dbed89f8e7..5cd5b4641b 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-1102" +tangemBlockchainSdk = "develop-1104" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-487" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 0142f675ad515a88773d0560b05bf7ea2fecce2a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 17:25:02 +0500 Subject: [PATCH 006/110] Updated on 2026-08-14 --- .../send/v2/common/CommonSendRoute.kt | 5 + .../features/send/v2/common/ui/SendContent.kt | 17 ++- .../send/v2/send/DefaultSendComponent.kt | 74 ++++++++++++- .../v2/send/confirm/SendConfirmComponent.kt | 1 + .../v2/send/confirm/model/SendConfirmModel.kt | 87 +++++++++------ .../v2/send/confirm/ui/SendConfirmContent.kt | 39 ++++--- .../send/v2/send/di/CommonSendModelModule.kt | 6 + .../features/send/v2/send/model/SendModel.kt | 5 +- .../success/SendConfirmSuccessComponent.kt | 51 +++++++++ .../success/model/SendConfirmSuccessModel.kt | 104 ++++++++++++++++++ .../success/ui/SendConfirmSuccessContent.kt | 92 ++++++++++++++++ .../features/send/v2/send/ui/state/SendUM.kt | 2 + 12 files changed, 428 insertions(+), 55 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt index 7aaa0a1286..ed717d0c97 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/CommonSendRoute.kt @@ -18,6 +18,11 @@ internal sealed class CommonSendRoute : Route { override val isEditMode: Boolean = true } + @Serializable + data object ConfirmSuccess : CommonSendRoute() { + override val isEditMode: Boolean = false + } + @Serializable data class Destination( override val isEditMode: Boolean, 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 5edf831c46..678d84dff3 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 @@ -6,8 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment 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.extensions.compose.stack.animation.* import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon @@ -15,6 +14,8 @@ 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( @@ -32,12 +33,20 @@ internal fun SendContent( SendAppBar(navigationUM = navigationUM) Children( stack = stackState, - animation = stackAnimation(slide()), + animation = stackAnimation { child -> + when (child.instance) { + is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f) + is SendConfirmComponent -> fade() + else -> slide() + } + }, modifier = Modifier.weight(1f), ) { it.instance.Content(Modifier.weight(1f)) } - SendNavigationButtons(navigationUM = navigationUM) + if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) { + SendNavigationButtons(navigationUM = navigationUM) + } } } 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 da4ee0e26c..dd7a643b22 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 @@ -14,6 +14,7 @@ import com.arkivanov.decompose.value.subscribe import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler 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 @@ -30,11 +31,14 @@ import com.tangem.features.send.v2.common.utils.safeNextClick import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent 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.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent 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.subcomponents.destination.DefaultSendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted @@ -44,6 +48,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.launch +@Suppress("LargeClass") internal class DefaultSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SendComponent.Params, @@ -142,7 +147,8 @@ internal class DefaultSendComponent @AssistedInject constructor( is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route) is CommonSendRoute.Amount -> getAmountComponent(factoryContext, route) is CommonSendRoute.Fee -> getFeeComponent(factoryContext) - CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + is CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + is CommonSendRoute.ConfirmSuccess -> getConfirmSuccessComponent(factoryContext) } private fun getDestinationComponent( @@ -288,6 +294,9 @@ internal class DefaultSendComponent @AssistedInject constructor( callback = model, predefinedValues = model.predefinedValues, onLoadFee = model::loadFee, + onSendTransaction = { + innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) + }, ), ) } else { @@ -296,6 +305,69 @@ internal class DefaultSendComponent @AssistedInject constructor( } } + private fun getConfirmSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val state = model.uiState.value + val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + 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 || + txUrl == null + ) { + model.showAlertError() + return getStubComponent() + } + + val destinationBlockComponent = + DefaultSendDestinationBlockComponent( + appComponentContext = child("sendConfirmDestinationBlock"), + params = SendDestinationComponentParams.DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = analyticCategoryName, + userWalletId = model.userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + blockClickEnableFlow = MutableStateFlow(true), + predefinedValues = model.predefinedValues, + ), + onResult = { }, + onClick = {}, + ) + + val feeBlockComponent = SendFeeBlockComponent( + appComponentContext = child("sendConfirmFeeBlock"), + params = SendFeeComponentParams.FeeBlockParams( + state = model.uiState.value.feeUM, + analyticsCategoryName = 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 = analyticCategoryName, + currentRoute = currentRoute, + txUrl = txUrl, + callback = model, + ), + ) + } + private fun getStubComponent() = StubComponent() class StubComponent : ComposableContentComponent { 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 997ad90f2a..df576bb18f 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 @@ -155,6 +155,7 @@ internal class SendConfirmComponent( val isBalanceHidingFlow: StateFlow, val predefinedValues: PredefinedValues, val onLoadFee: suspend () -> Either, + val onSendTransaction: () -> Unit, ) interface ModelCallback { 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 f8abef07da..e4bfe5b26f 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 @@ -291,6 +291,7 @@ internal class SendConfirmModel @Inject constructor( isShowTapHelp = isShowTapHelp, walletName = stringReference(userWallet.name), ).transform(uiState.value.confirmUM), + confirmData = confirmData, ) } updateConfirmNotifications() @@ -384,6 +385,10 @@ internal class SendConfirmModel @Inject constructor( addTokenToWalletIfNeeded() sendBalanceUpdater.scheduleUpdates() sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + if (uiState.value.isRedesignEnabled) { + params.callback.onResult(uiState.value) + params.onSendTransaction() + } }, ) } @@ -462,23 +467,35 @@ internal class SendConfirmModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).filter { it.second is CommonSendRoute.Confirm }.onEach { (state, _) -> + ).filter { + it.second is CommonSendRoute.Confirm + }.onEach { (state, _) -> val amountUM = state.amountUM as? AmountState.Data val confirmUM = state.confirmUM - val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( - title = resourceReference( - id = R.string.send_summary_title, - formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), - ), + title = if (state.isRedesignEnabled) { + stringReference("") + } else { + resourceReference( + id = R.string.send_summary_title, + formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), + ) + }, subtitle = if (uiState.value.isRedesignEnabled) { null } else { amountUM?.title }, - backIconRes = R.drawable.ic_close_24, + 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( @@ -490,31 +507,7 @@ internal class SendConfirmModel @Inject constructor( ) 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.takeIf { isReadyToSend }, - isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = isReadyToSend, - onClick = { - when (confirmUM) { - is ConfirmUM.Success -> appRouter.pop() - is ConfirmUM.Content -> if (confirmUM.isSending) { - return@NavigationButton - } else { - onSendClick() - } - else -> return@NavigationButton - } - }, - ), + primaryButton = primaryButtonUM(), prevButton = null, secondaryPairButtonsUM = ( NavigationButton( @@ -533,6 +526,36 @@ internal class SendConfirmModel @Inject constructor( }.launchIn(modelScope) } + 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) + } + else -> resourceReference(R.string.common_send) + }, + iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = isReadyToSend, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> appRouter.pop() + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ) + } + private companion object { const val CHECK_FEE_UPDATE_DELAY = 10_000L } 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 8dc774afd7..738c7e22ab 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 @@ -82,31 +82,36 @@ private fun LazyListScope.blocks( ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - AnimatedVisibility( - visible = uiState.confirmUM is ConfirmUM.Success, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) { - val wrappedConfirmUM = remember(this) { uiState.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 (uiState.isRedesignEnabled) { amountBlockComponent.Content(modifier = Modifier) destinationBlockComponent.Content(modifier = Modifier) } else { + TransactionDoneTitleAnimated(uiState) destinationBlockComponent.Content(modifier = Modifier) amountBlockComponent.Content(modifier = Modifier) } feeBlockComponent.Content(modifier = Modifier) } } +} + +@Composable +internal fun TransactionDoneTitleAnimated(uiState: SendUM) { + AnimatedVisibility( + visible = uiState.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = 12.dp), + ) { + val wrappedConfirmUM = remember(this) { uiState.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/send/di/CommonSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.kt index d2201019ff..bff655e61d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/di/CommonSendModelModule.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.send.confirm.model.SendConfirmModel import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +24,9 @@ internal interface CommonSendModelModule { @IntoMap @ClassKey(SendConfirmModel::class) fun provideSendConfirmModel(model: SendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(SendConfirmSuccessModel::class) + fun provideSendConfirmSuccessModel(model: SendConfirmSuccessModel): Model } \ 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 c573aad0bd..3ffc5b25e0 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 @@ -46,6 +46,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.send.confirm.SendConfirmComponent +import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger @@ -65,7 +66,8 @@ internal interface SendComponentCallback : SendAmountComponent.ModelCallback, SendFeeComponent.ModelCallback, SendDestinationComponent.ModelCallback, - SendConfirmComponent.ModelCallback + SendConfirmComponent.ModelCallback, + SendConfirmSuccessComponent.ModelCallback @Stable @ModelScoped @@ -382,5 +384,6 @@ internal class SendModel @Inject constructor( confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, + confirmData = null, ) } \ No newline at end of file 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 new file mode 100644 index 0000000000..c9fa321928 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt @@ -0,0 +1,51 @@ +package com.tangem.features.send.v2.send.success + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +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.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +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 + +internal class SendConfirmSuccessComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + 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) { + val state by model.uiState.collectAsState() + 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, + val callback: ModelCallback, + ) + + 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/send/success/model/SendConfirmSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt new file mode 100644 index 0000000000..663d111b4f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/model/SendConfirmSuccessModel.kt @@ -0,0 +1,104 @@ +package com.tangem.features.send.v2.send.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.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 javax.inject.Inject + +@Stable +@ModelScoped +internal class SendConfirmSuccessModel @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: SendConfirmSuccessComponent.Params = paramsContainer.require() + private val _uiState = params.sendUMFlow + val uiState = _uiState + + 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/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt new file mode 100644 index 0000000000..862d4e1483 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.send.success.ui + +import androidx.compose.animation.* +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.rememberScrollState +import androidx.compose.runtime.* +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.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.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 +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import kotlinx.coroutines.delay + +@Composable +internal fun SendConfirmSuccessContent( + sendUM: SendUM, + destinationBlockComponent: SendDestinationBlockComponent, + feeBlockComponent: SendFeeBlockComponent, +) { + 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", + ) { + Column { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Horizontal, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (sendUM.confirmUM is ConfirmUM.Success) { + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + sendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + sendUM.confirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + AmountBlock( + amountState = sendUM.amountUM, + isClickDisabled = true, + isEditingDisabled = true, + onClick = {}, + ) + destinationBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) + } + SpacerHMax() + SendNavigationButtons(navigationUM = sendUM.navigationUM) + } + } +} + +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/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index 963bec1cf2..df436f0170 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -4,6 +4,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationUM 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.send.confirm.model.ConfirmData import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class SendUM( @@ -13,4 +14,5 @@ internal data class SendUM( val confirmUM: ConfirmUM, val navigationUM: NavigationUM, val isRedesignEnabled: Boolean, + val confirmData: ConfirmData?, ) \ No newline at end of file From 99d7eec8cd383eafefc0ad5d4693497d52af5b89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 09:59:33 +0400 Subject: [PATCH 007/110] Updated on 2026-08-14 --- .../topup/model/OnboardingNoteTopUpModel.kt | 15 ++++++-- .../v2/twin/impl/model/OnboardingTwinModel.kt | 28 +++++++++----- .../wallet/child/wallet/model/WalletModel.kt | 17 +++++++-- .../model/intents/WalletClickIntents.kt | 37 ++++++++++++++----- 4 files changed, 69 insertions(+), 28 deletions(-) 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 29d8601ba0..cbaed64847 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 @@ -19,9 +19,11 @@ import com.tangem.domain.models.scan.ScanResponse 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.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -32,6 +34,7 @@ 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") @@ -49,6 +52,8 @@ internal class OnboardingNoteTopUpModel @Inject constructor( 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() @@ -83,10 +88,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor( showBalanceLoadingProgress(true) createUserWalletIfNull() val userWalletId = requireNotNull(userWallet?.walletId) - fetchCurrencyStatusUseCase( - userWalletId = userWalletId, - refresh = true, - ) + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + .onLeft(Timber::e) + } else { + fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true) + } showBalanceLoadingProgress(false) } } 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 f4b803c505..ca88682ad0 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 @@ -36,8 +36,10 @@ 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 @@ -85,6 +87,8 @@ internal class OnboardingTwinModel @Inject constructor( 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() @@ -329,13 +333,15 @@ internal class OnboardingTwinModel @Inject constructor( cardRepository.finishCardActivation(params.scanResponse.card.cardId) - fetchCurrencyStatusUseCase.invoke( - userWalletId = userWallet.walletId, - refresh = true, - ).onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) + 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() @@ -431,10 +437,12 @@ internal class OnboardingTwinModel @Inject constructor( it.copy(isLoading = true) } modelScope.launch { - fetchCurrencyStatusUseCase( - userWalletId = userWallet.walletId, - refresh = true, - ) + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) + .onLeft(Timber::e) + } else { + fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index fe3e58ff9e..287c272322 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -20,6 +20,8 @@ import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency @@ -83,6 +85,8 @@ internal class WalletModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val deepLinksRegistry: DeepLinksRegistry, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val walletBalanceFetcher: WalletBalanceFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, private val appRouter: AppRouter, private val routingFeatureToggle: RoutingFeatureToggle, private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, @@ -518,10 +522,15 @@ internal class WalletModel @Inject constructor( } private fun fetchIfSingleWallet(userWallet: UserWallet) { - if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { - modelScope.launch { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) - .onLeft { Timber.e(it.toString()) } + modelScope.launch { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) + .onLeft(Timber::e) + } else { + if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { + fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) + .onLeft { Timber.e(it.toString()) } + } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 1537149e1a..759a4ac87b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -11,6 +11,9 @@ import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode +import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase @@ -23,11 +26,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetRefreshStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer import com.tangem.features.onramp.OnrampFeatureToggles -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -44,12 +47,13 @@ internal class WalletClickIntents @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, + private val walletBalanceFetcher: WalletBalanceFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, private val rampStateManager: RampStateManager, - private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, private val onrampStatusFactory: OnrampStatusFactory, @@ -127,13 +131,21 @@ internal class WalletClickIntents @Inject constructor( ) modelScope.launch { - val maybeFetchResult = if ( - userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - ) { - fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + val maybeFetchResult = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletBalanceFetcher.invoke(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) + .mapLeft { + Timber.e(it) + TokenListError.DataError(it) + } } else { - fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) + val isSingleWalletWithToken = userWallet is UserWallet.Cold && + userWallet.cardTypesResolver.isSingleWalletWithToken() + + if (isSingleWalletWithToken) { + fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + } else { + fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) + } } buildList { @@ -172,8 +184,13 @@ internal class WalletClickIntents @Inject constructor( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) - modelScope.launch(dispatchers.main) { - fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) + modelScope.launch { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) + .onLeft(Timber::e) + } else { + fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) + } onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, From e6530aa1ee1a3f58a69d5627815d73854e3b1975 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 20:53:17 +0500 Subject: [PATCH 008/110] Updated on 2026-08-14 --- .../DefaultWalletConnectEntryComponent.kt | 4 ++-- ...ionsComponent.kt => WcConnectionsComponent.kt} | 15 ++++++++++++--- .../connections/model/WcConnectionsModel.kt | 4 ++-- .../connections/ui/WcConnectionsContent.kt | 13 ++++++++++++- 4 files changed, 28 insertions(+), 8 deletions(-) rename features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/{ConnectionsComponent.kt => WcConnectionsComponent.kt} (76%) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt index f6cce51fd1..2b835ba928 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt @@ -47,9 +47,9 @@ internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( config: ConnectionsInnerRoute, componentContext: ComponentContext, ): ComposableContentComponent = when (config) { - ConnectionsInnerRoute.Connections -> ConnectionsComponent( + ConnectionsInnerRoute.Connections -> WcConnectionsComponent( appComponentContext = childByContext(componentContext), - params = ConnectionsComponent.Params(params.userWalletId), + params = WcConnectionsComponent.Params(params.userWalletId), ) ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]") } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt similarity index 76% rename from features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt rename to features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt index 45e927da97..be19779bad 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt @@ -1,7 +1,9 @@ package com.tangem.features.walletconnect.connections.components +import androidx.compose.material3.SnackbarHostState 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.arkivanov.decompose.ComponentContext @@ -13,16 +15,19 @@ 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.core.ui.message.EventMessageEffect +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.connections.model.WcConnectionsModel import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent -internal class ConnectionsComponent( +internal class WcConnectionsComponent( appComponentContext: AppComponentContext, params: Params, ) : AppComponentContext by appComponentContext, ComposableContentComponent { + private val messageHandler = EventMessageHandler() private val model: WcConnectionsModel = getOrCreateModel(params) private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, @@ -35,8 +40,11 @@ internal class ConnectionsComponent( override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() - WcConnectionsContent(modifier = modifier, state = state) + val snackbarHostState = remember { SnackbarHostState() } + WcConnectionsContent(modifier = modifier, state = state, snackbarHostState = snackbarHostState) + + EventMessageEffect(messageHandler = messageHandler, snackbarHostState = snackbarHostState) bottomSheet.child?.instance?.BottomSheet() } @@ -44,9 +52,10 @@ internal class ConnectionsComponent( config: WcConnectionsBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent { + val context = childByContext(componentContext = componentContext, messageHandler = messageHandler) return when (config) { is WcConnectionsBottomSheetConfig.ConnectedApp -> WcConnectedAppInfoComponent( - appComponentContext = childByContext(componentContext), + appComponentContext = context, params = WcConnectedAppInfoComponent.Params( topic = config.topic, onDismiss = model.bottomSheetNavigation::dismiss, 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 0c7c5da768..30525785c2 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 @@ -22,7 +22,7 @@ import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase -import com.tangem.features.walletconnect.connections.components.ConnectionsComponent +import com.tangem.features.walletconnect.connections.components.WcConnectionsComponent import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer @@ -48,7 +48,7 @@ internal class WcConnectionsModel @Inject constructor( paramsContainer: ParamsContainer, ) : Model() { - private val params = paramsContainer.require() + private val params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(getInitialState()) val bottomSheetNavigation: SlotNavigation = SlotNavigation() 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 e529ec9d02..f3c8baef60 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 com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -41,10 +42,20 @@ import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList @Composable -internal fun WcConnectionsContent(state: WcConnectionsState, modifier: Modifier = Modifier) { +internal fun WcConnectionsContent( + state: WcConnectionsState, + modifier: Modifier = Modifier, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, +) { Scaffold( modifier = modifier, containerColor = TangemTheme.colors.background.secondary, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + hostState = snackbarHostState, + ) + }, topBar = { ConnectionsTopBar( modifier = Modifier.statusBarsPadding(), From 584be65dd2ca88928b44a5b4a0621069c61d3405 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Jul 2025 17:26:16 +0400 Subject: [PATCH 009/110] Updated on 2026-08-14 --- .../currency/CardCryptoCurrencyFactory.kt | 13 +- .../DefaultCardCryptoCurrencyFactory.kt | 34 +-- .../DefaultCardCryptoCurrencyFactoryTest.kt | 4 +- .../multi/DefaultMultiNetworkStatusFetcher.kt | 45 +--- .../DefaultSingleNetworkStatusFetcher.kt | 36 +-- .../DefaultMultiNetworkStatusFetcherTest.kt | 232 +++--------------- .../DefaultSingleNetworkStatusFetcherTest.kt | 108 ++------ 7 files changed, 118 insertions(+), 354 deletions(-) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt index 83d76a648e..137e425ac6 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt @@ -20,7 +20,18 @@ interface CardCryptoCurrencyFactory { * @param network network */ @Throws - suspend fun create(userWalletId: UserWalletId, network: Network): List + suspend fun create(userWalletId: UserWalletId, network: Network): List { + return create(userWalletId = userWalletId, networks = setOf(network))[network].orEmpty() + } + + /** + * Universal method for creating list of [CryptoCurrency] in [networks] for any card + * + * @param userWalletId user wallet id that determines type of card + * @param networks networks + */ + @Throws + suspend fun create(userWalletId: UserWalletId, networks: Set): Map> /** * Universal method for creating list of [CryptoCurrency] in [network] for any card diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index 286947afa4..58b6c3415b 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -2,7 +2,6 @@ package com.tangem.data.common.currency import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.local.token.UserTokensResponseStore @@ -36,40 +35,45 @@ internal class DefaultCardCryptoCurrencyFactory( private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory(excludedBlockchains) } - override suspend fun create(userWalletId: UserWalletId, network: Network): List { + override suspend fun create( + userWalletId: UserWalletId, + networks: Set, + ): Map> { val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) - val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) - // multi-currency wallet if (userWallet !is UserWallet.Cold || userWallet.isMultiCurrency) { - return getMultiWalletCurrencies(userWallet = userWallet, networks = setOf(network))[network].orEmpty() + return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) } // check if the blockchain of single-currency wallet is the same as network - val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - if (cardBlockchain != blockchain) return emptyList() + val cardNetworkId = userWallet.scanResponse.cardTypesResolver.getBlockchain().toNetworkId() + val cardNetwork = networks.firstOrNull { it.backendId == cardNetworkId } + + if (cardNetwork == null) return emptyMap() // single-currency wallet with token (NODL) if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + val currencies = createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + return mapOf(cardNetwork to currencies) } // single-currency wallet - return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + val primaryCurrency = createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + return mapOf(cardNetwork to listOf(primaryCurrency)) } - override suspend fun createByRawId(userWalletId: UserWalletId, networkRawId: Network.RawID): List { + override suspend fun createByRawId(userWalletId: UserWalletId, network: Network.RawID): List { val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) - val blockchain = networkRawId.toBlockchain() + val blockchain = network.toBlockchain() // multi-currency wallet if (userWallet.isMultiCurrency || userWallet !is UserWallet.Cold) { return getMultiWalletCurrenciesByRawId( userWallet = userWallet, - rawIds = setOf(networkRawId), - )[networkRawId].orEmpty() + rawIds = setOf(network), + )[network].orEmpty() } // check if the blockchain of single-currency wallet is the same as network @@ -143,13 +147,15 @@ internal class DefaultCardCryptoCurrencyFactory( val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) ?: return emptyMap() - return responseCryptoCurrenciesFactory.createCurrencies( + val existingNetworkWithCurrencies = responseCryptoCurrenciesFactory.createCurrencies( tokens = response.tokens.filter { token -> networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } }, scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] ) .groupBy(CryptoCurrency::network) + + return networks.associateWith { emptyList() } + existingNetworkWithCurrencies } private suspend fun getMultiWalletCurrenciesByRawId( diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 0622887743..944a461dee 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -266,7 +266,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), userTokensResponse = createUserTokensResponse(), - expected = Result.success(emptyMap()), + expected = Result.success( + setOf(ethereum.network, bitcoin.network).associateWith { emptyList() }, + ), ), CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index aa05c31589..d94944b2a4 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -7,11 +7,10 @@ import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -22,7 +21,6 @@ import javax.inject.Inject * Default implementation of [MultiNetworkStatusFetcher] * * @property networksStatusesStore networks statuses store - * @property userWalletsStore user wallets store * @property cardCryptoCurrencyFactory card crypto currency factory * @property commonNetworkStatusFetcher common network status fetcher * @property dispatchers dispatchers @@ -32,7 +30,6 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val networksStatusesStore: NetworksStatusesStore, - private val userWalletsStore: UserWalletsStore, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, private val dispatchers: CoroutineDispatcherProvider, @@ -41,8 +38,8 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) { networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, networks = params.networks) - val userWallet = catch( - block = { userWalletsStore.getSyncStrict(key = params.userWalletId) }, + val networksCurrencies = catch( + block = { createNetworksCurrenciesMap(params) }, catch = { networksStatusesStore.setSourceAsOnlyCache( userWalletId = params.userWalletId, @@ -53,31 +50,6 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( }, ) - val cardTypesResolver = userWallet.requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY] - val isWalletSupported = with(cardTypesResolver) { - isMultiwalletAllowed() || isSingleWalletWithToken() - } - - ensure(isWalletSupported) { - networksStatusesStore.setSourceAsOnlyCache( - userWalletId = params.userWalletId, - networks = params.networks, - ) - IllegalStateException("User wallet is not multi-currency") - } - - val networksCurrencies = if (cardTypesResolver.isMultiwalletAllowed()) { - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard( - userWallet = userWallet, - networks = params.networks, - ) - } else { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( - scanResponse = userWallet.scanResponse, - ) - .groupBy { it.network } - } - val result = coroutineScope { params.networks .map { network -> @@ -98,4 +70,13 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( IllegalStateException("Failed to fetch network statuses") } } + + private suspend fun createNetworksCurrenciesMap( + params: MultiNetworkStatusFetcher.Params, + ): Map> { + return cardCryptoCurrencyFactory.create( + userWalletId = params.userWalletId, + networks = params.networks, + ) + } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 4db17e1818..c89ba87fe5 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -1,47 +1,27 @@ package com.tangem.data.networks.single import arrow.core.Either -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher -import com.tangem.data.networks.store.NetworksStatusesStore -import com.tangem.data.networks.store.setSourceAsCache -import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject /** * Default implementation of [SingleNetworkStatusFetcher] * - * @property commonNetworkStatusFetcher common network status fetcher - * @property networksStatusesStore networks statuses store - * @property cardCryptoCurrencyFactory card crypto currency factory - * @property dispatchers dispatchers + * @property multiNetworkStatusFetcher multi network status fetcher * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( - private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, - private val networksStatusesStore: NetworksStatusesStore, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val dispatchers: CoroutineDispatcherProvider, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ) : SingleNetworkStatusFetcher { override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either { - return Either.catchOn(dispatchers.default) { - networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) - - val networkCurrencies = cardCryptoCurrencyFactory.create( + return multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params( userWalletId = params.userWalletId, - network = params.network, - ) - - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = params.network, - networkCurrencies = networkCurrencies.toSet(), - ) - .onLeft { throw it } - } + networks = setOf(params.network), + ), + ) } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index 4284eb4610..3bad3aaceb 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -1,19 +1,15 @@ package com.tangem.data.networks.multi import arrow.core.Either -import com.google.common.truth.Truth +import arrow.core.left import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.test.utils.assertEither import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -29,13 +25,11 @@ import org.junit.jupiter.api.TestInstance internal class DefaultMultiNetworkStatusFetcherTest { private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true) - private val userWalletsStore: UserWalletsStore = mockk() private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() private val fetcher = DefaultMultiNetworkStatusFetcher( networksStatusesStore = networksStatusesStore, - userWalletsStore = userWalletsStore, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, commonNetworkStatusFetcher = commonNetworkStatusFetcher, dispatchers = TestingCoroutineDispatcherProvider(), @@ -43,16 +37,17 @@ internal class DefaultMultiNetworkStatusFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(networksStatusesStore, userWalletsStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) + clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) } @Test - fun `fetch successfully for multi-currency card`() = runTest { + fun `fetch successfully`() = runTest { // Arrange - val networks = setOf(ethereum.network, cardano.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWallet = MockUserWalletFactory.create() - val cardTypesResolver = mockk() + val params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = setOf(ethereum.network, cardano.network), + ) + val networksCurrencies = mapOf( ethereum.network to listOf(ethereum), cardano.network to listOf(cardano), @@ -60,16 +55,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethereumFetcherResult = Either.Right(Unit) val cardanoFetcherResult = Either.Right(Unit) - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet - - mockkStatic(UserWallet.Cold::cardTypesResolver) - every { userWallet.cardTypesResolver } returns cardTypesResolver - coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true - - coEvery { - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) - } returns networksCurrencies + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies coEvery { commonNetworkStatusFetcher.fetch( @@ -92,14 +78,11 @@ internal class DefaultMultiNetworkStatusFetcherTest { // Assert val expected = Either.Right(Unit) - - Truth.assertThat(actual).isEqualTo(expected) + assertEither(actual, expected) coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) - cardTypesResolver.isMultiwalletAllowed() - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = ethereum.network, @@ -114,75 +97,17 @@ internal class DefaultMultiNetworkStatusFetcherTest { coVerify(inverse = true) { networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) - cardTypesResolver.isSingleWalletWithToken() - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) - } - } - - @Test - fun `fetch successfully for single-currency card with token`() = runTest { - // Arrange - val networks = setOf(ethereum.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWallet = MockUserWalletFactory.create() - val cardTypesResolver = mockk() - val networksCurrencies = listOf(ethereum) - val ethereumFetcherResult = Either.Right(Unit) - - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet - - mockkStatic(UserWallet.Cold::cardTypesResolver) - every { userWallet.cardTypesResolver } returns cardTypesResolver - coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false - coEvery { cardTypesResolver.isSingleWalletWithToken() } returns true - - coEvery { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) - } returns networksCurrencies - - coEvery { - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = ethereum.network, - networkCurrencies = setOf(ethereum), - ) - } returns ethereumFetcherResult - - // Act - val actual = fetcher(params) - - // Assert - val expected = Either.Right(Unit) - - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { - networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) - cardTypesResolver.isMultiwalletAllowed() - cardTypesResolver.isSingleWalletWithToken() - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = ethereum.network, - networkCurrencies = setOf(ethereum), - ) - } - - coVerify(inverse = true) { - networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) } } @Test fun `fetch failure if one of them fails`() = runTest { // Arrange - val networks = setOf(ethereum.network, cardano.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWallet = MockUserWalletFactory.create() - val cardTypesResolver = mockk() + val params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = setOf(ethereum.network, cardano.network), + ) + val networksCurrencies = mapOf( ethereum.network to listOf(ethereum), cardano.network to listOf(cardano), @@ -190,16 +115,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethereumFetcherResult = Either.Left(IllegalStateException()) val cardanoFetcherResult = Either.Right(Unit) - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet - - mockkStatic(UserWallet.Cold::cardTypesResolver) - every { userWallet.cardTypesResolver } returns cardTypesResolver - coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true - - coEvery { - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) - } returns networksCurrencies + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies coEvery { commonNetworkStatusFetcher.fetch( @@ -222,15 +138,11 @@ internal class DefaultMultiNetworkStatusFetcherTest { // Assert val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + assertEither(actual, expected) coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) - cardTypesResolver.isMultiwalletAllowed() - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = ethereum.network, @@ -245,18 +157,17 @@ internal class DefaultMultiNetworkStatusFetcherTest { coVerify(inverse = true) { networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) - cardTypesResolver.isSingleWalletWithToken() - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) } } @Test fun `fetch failure if all of them fails`() = runTest { // Arrange - val networks = setOf(ethereum.network, cardano.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWallet = MockUserWalletFactory.create() - val cardTypesResolver = mockk() + val params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = setOf(ethereum.network, cardano.network), + ) + val networksCurrencies = mapOf( ethereum.network to listOf(ethereum), cardano.network to listOf(cardano), @@ -264,16 +175,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethereumFetcherResult = Either.Left(IllegalStateException("ethereum")) val cardanoFetcherResult = Either.Left(IllegalStateException("cardano")) - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet - - mockkStatic(UserWallet.Cold::cardTypesResolver) - every { userWallet.cardTypesResolver } returns cardTypesResolver - coEvery { cardTypesResolver.isMultiwalletAllowed() } returns true - - coEvery { - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) - } returns networksCurrencies + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } returns networksCurrencies coEvery { commonNetworkStatusFetcher.fetch( @@ -296,15 +198,11 @@ internal class DefaultMultiNetworkStatusFetcherTest { // Assert val expected = Either.Left(IllegalStateException("Failed to fetch network statuses")) - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) + assertEither(actual, expected) coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) - cardTypesResolver.isMultiwalletAllowed() - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(userWallet, params.networks) + cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = ethereum.network, @@ -319,85 +217,35 @@ internal class DefaultMultiNetworkStatusFetcherTest { coVerify(inverse = true) { networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) - cardTypesResolver.isSingleWalletWithToken() - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) } } @Test - fun `fetch failure if userWalletsStore throws exception`() = runTest { + fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest { // Arrange - val networks = setOf(ethereum.network, cardano.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWalletStoreException = IllegalStateException() + val params = MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = setOf(ethereum.network, cardano.network), + ) - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } throws userWalletStoreException - coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit + val factoryException = IllegalStateException() + + coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) } throws factoryException // Act val actual = fetcher(params) // Assert - val expected = Either.Left(userWalletStoreException) - - Truth.assertThat(actual).isEqualTo(expected) + val expected = factoryException.left() + assertEither(actual, expected) coVerifyOrder { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) + cardCryptoCurrencyFactory.create(params.userWalletId, params.networks) networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } - coVerify(inverse = true) { - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) - commonNetworkStatusFetcher.fetch(any(), any(), any()) - } - } - - @Test - fun `fetch failure if card is single-currency`() = runTest { - // Arrange - val networks = setOf(ethereum.network, cardano.network) - val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks) - val userWallet = MockUserWalletFactory.create() - val cardTypesResolver = mockk() - - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) } returns Unit - every { userWalletsStore.getSyncStrict(key = userWalletId) } returns userWallet - - mockkStatic(UserWallet.Cold::cardTypesResolver) - every { userWallet.cardTypesResolver } returns cardTypesResolver - coEvery { cardTypesResolver.isMultiwalletAllowed() } returns false - coEvery { cardTypesResolver.isSingleWalletWithToken() } returns false - - coEvery { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } returns Unit - - // Act - val actual = fetcher(params) - - // Assert - val expected = Either.Left(IllegalStateException("User wallet is not multi-currency")) - - Truth.assertThat(actual.isLeft()).isTrue() - Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected.leftOrNull()!!::class.java) - Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.leftOrNull()!!.message) - - coVerifyOrder { - networksStatusesStore.setSourceAsCache(params.userWalletId, params.networks) - userWalletsStore.getSyncStrict(key = userWalletId) - cardTypesResolver.isMultiwalletAllowed() - cardTypesResolver.isSingleWalletWithToken() - networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) - } - - coVerify(inverse = true) { - networksStatusesStore.setSourceAsOnlyCache(userWalletId = any(), networks = any()) - cardCryptoCurrencyFactory.createCurrenciesForMultiCurrencyCard(any(), any()) - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = any()) - commonNetworkStatusFetcher.fetch(any(), any(), any()) - } + coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) } } private companion object { diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index 979f32b859..f994a4bbeb 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -2,16 +2,15 @@ package com.tangem.data.networks.single import arrow.core.Either import arrow.core.left -import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher -import com.tangem.data.networks.store.NetworksStatusesStore -import com.tangem.data.networks.store.setSourceAsCache +import com.tangem.common.test.utils.assertEither +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +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 @@ -23,114 +22,51 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultSingleNetworkStatusFetcherTest { - private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() - private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true) - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() - private val fetcher = DefaultSingleNetworkStatusFetcher( - commonNetworkStatusFetcher = commonNetworkStatusFetcher, - networksStatusesStore = networksStatusesStore, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - dispatchers = TestingCoroutineDispatcherProvider(), - ) + private val fetcher = DefaultSingleNetworkStatusFetcher(multiNetworkStatusFetcher = multiNetworkStatusFetcher) @BeforeEach fun resetMocks() { - clearMocks(commonNetworkStatusFetcher, networksStatusesStore, cardCryptoCurrencyFactory) + clearMocks(multiNetworkStatusFetcher) } @Test fun `fetch successfully`() = runTest { // Arrange val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) - val networkCurrencies = listOf(ethereum) - val commonFetcherResult = Either.Right(Unit) + val multiParams = MultiNetworkStatusFetcher.Params(userWalletId, setOf(params.network)) + val multiFetcherResult = Either.Right(Unit) - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies - coEvery { - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = params.network, - networkCurrencies = networkCurrencies.toSet(), - ) - } returns commonFetcherResult + coEvery { multiNetworkStatusFetcher(params = multiParams) } returns multiFetcherResult // Act val actual = fetcher(params) // Assert - val expected = commonFetcherResult + val expected = multiFetcherResult + assertEither(actual, expected) - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { - networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) - cardCryptoCurrencyFactory.create(params.userWalletId, params.network) - commonNetworkStatusFetcher.fetch(params.userWalletId, params.network, setOf(ethereum)) - } + coVerify(exactly = 1) { multiNetworkStatusFetcher(params = multiParams) } } @Test - fun `fetch failure if cardCryptoCurrencyFactory throws exception`() = runTest { + fun `fetch failure`() = runTest { // Arrange val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) - val factoryException = IllegalStateException() + val multiParams = MultiNetworkStatusFetcher.Params(userWalletId, setOf(params.network)) + val multiFetcherResult = IllegalStateException("Error").left() - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } throws factoryException + coEvery { multiNetworkStatusFetcher(params = multiParams) } returns multiFetcherResult // Act val actual = fetcher(params) // Arrange - val expected = Either.Left(factoryException) - Truth.assertThat(actual).isEqualTo(expected) + val expected = multiFetcherResult + assertEither(actual, expected) - coVerifyOrder { - networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) - cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) - } - - coVerify(inverse = true) { - commonNetworkStatusFetcher.fetch(userWalletId = any(), network = any(), networkCurrencies = any()) - } - } - - @Test - fun `fetch failure if commonNetworkStatusFetcher returns exception`() = runTest { - // Arrange - val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = ethereum.network) - val networkCurrencies = listOf(ethereum) - val commonFetcherResult = IllegalStateException().left() - - coEvery { networksStatusesStore.setSourceAsCache(params.userWalletId, params.network) } returns Unit - coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns networkCurrencies - coEvery { - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = params.network, - networkCurrencies = networkCurrencies.toSet(), - ) - } returns commonFetcherResult - - // Act - val actual = fetcher(params) - - // Arrange - val expected = commonFetcherResult - - Truth.assertThat(actual).isEqualTo(expected) - - coVerifyOrder { - networksStatusesStore.setSourceAsCache(userWalletId = params.userWalletId, network = params.network) - cardCryptoCurrencyFactory.create(userWalletId = params.userWalletId, network = params.network) - commonNetworkStatusFetcher.fetch( - userWalletId = params.userWalletId, - network = params.network, - networkCurrencies = setOf(ethereum), - ) - } + coVerify(exactly = 1) { multiNetworkStatusFetcher(params = multiParams) } } private companion object { From 64bea3762541173111fab8bd0b9be7e164f71845 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 00:22:29 +0500 Subject: [PATCH 010/110] Updated on 2026-08-14 --- .../ui/CreateMobileWalletContent.kt | 2 +- .../start/ManualBackupStartComponent.kt | 36 +++++ .../start/ManualBackupStartModel.kt | 26 ++++ .../start/entity/ManualBackupStartUM.kt | 6 + .../start/ui/ManualBackupStartContent.kt | 138 ++++++++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/entity/ManualBackupStartUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index 5de3f84a92..d6861cb49e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -84,7 +84,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo modifier = Modifier .fillMaxWidth() .padding(16.dp), - text = "Create", + text = stringResourceSafe(R.string.common_create), showProgress = false, enabled = true, onClick = state.onCreateClick, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartComponent.kt new file mode 100644 index 0000000000..4a300c9212 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.hotwallet.manualbackup.start + +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.hotwallet.manualbackup.start.ui.ManualBackupStartContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class ManualBackupStartComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: ManualBackupStartModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ManualBackupStartContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onContinueClick() + } + + data class Params( + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartModel.kt new file mode 100644 index 0000000000..ef4f85294b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ManualBackupStartModel.kt @@ -0,0 +1,26 @@ +package com.tangem.features.hotwallet.manualbackup.start + +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.manualbackup.start.entity.ManualBackupStartUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class ManualBackupStartModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: ManualBackupStartComponent.Params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + ManualBackupStartUM( + onContinueClick = params.callbacks::onContinueClick, + ), + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/entity/ManualBackupStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/entity/ManualBackupStartUM.kt new file mode 100644 index 0000000000..55ff6c0e45 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/entity/ManualBackupStartUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.hotwallet.manualbackup.start.entity + +internal data class ManualBackupStartUM( + val seepPhraseLength: Int = 12, + val onContinueClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt new file mode 100644 index 0000000000..435a9dfbb3 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/start/ui/ManualBackupStartContent.kt @@ -0,0 +1,138 @@ +package com.tangem.features.hotwallet.manualbackup.start.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +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.manualbackup.start.entity.ManualBackupStartUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe(R.string.backup_info_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 8.dp, + ), + text = stringResourceSafe( + R.string.backup_info_description, + state.seepPhraseLength.toString(), + ), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_save_title), + description = stringResourceSafe( + R.string.backup_info_save_description, + state.seepPhraseLength.toString(), + ), + iconRes = R.drawable.ic_lock_24, + ) + FeatureBlock( + modifier = Modifier + .padding(top = 24.dp), + title = stringResourceSafe(R.string.backup_info_keep_title), + description = stringResourceSafe(R.string.backup_info_keep_description), + iconRes = R.drawable.ic_settings_24, + ) + Spacer(modifier = Modifier.weight(1f)) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + text = stringResourceSafe(R.string.common_continue), + showProgress = false, + enabled = true, + onClick = state.onContinueClick, + ) + } +} + +@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 PreviewManualBackupStartContent() { + TangemThemePreview { + ManualBackupStartContent( + state = ManualBackupStartUM( + onContinueClick = {}, + ), + ) + } +} \ No newline at end of file From 9462ae606c42dc3836e6090a4132d03317bce107 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 00:23:13 +0500 Subject: [PATCH 011/110] Updated on 2026-08-14 --- .../ManualBackupCompletedComponent.kt | 36 ++++++++ .../completed/ManualBackupCompletedModel.kt | 26 ++++++ .../entity/ManualBackupCompletedUM.kt | 5 ++ .../ui/ManualBackupCompletedContent.kt | 86 +++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/entity/ManualBackupCompletedUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ui/ManualBackupCompletedContent.kt 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 new file mode 100644 index 0000000000..43f8c076d3 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.hotwallet.manualbackup.completed + +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.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class ManualBackupCompletedComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: ManualBackupCompletedModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ManualBackupCompletedContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onContinueClick() + } + + data class Params( + 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 new file mode 100644 index 0000000000..0646d7646b --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ManualBackupCompletedModel.kt @@ -0,0 +1,26 @@ +package com.tangem.features.hotwallet.manualbackup.completed + +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.manualbackup.completed.entity.ManualBackupCompletedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class ManualBackupCompletedModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: ManualBackupCompletedComponent.Params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + ManualBackupCompletedUM( + onContinueClick = params.callbacks::onContinueClick, + ), + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/entity/ManualBackupCompletedUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/entity/ManualBackupCompletedUM.kt new file mode 100644 index 0000000000..d45f370259 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/entity/ManualBackupCompletedUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet.manualbackup.completed.entity + +internal data class ManualBackupCompletedUM( + val onContinueClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ui/ManualBackupCompletedContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ui/ManualBackupCompletedContent.kt new file mode 100644 index 0000000000..77535d8e94 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/completed/ui/ManualBackupCompletedContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.hotwallet.manualbackup.completed.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.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.ic_success_blue_76), + contentDescription = null, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 48.dp, + top = 20.dp, + end = 48.dp, + ), + text = stringResourceSafe(R.string.backup_complete_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 48.dp, + top = 12.dp, + end = 48.dp, + ), + text = stringResourceSafe(R.string.backup_complete_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(2f)) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + showProgress = false, + enabled = true, + onClick = state.onContinueClick, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewManualBackupCompletedContent() { + TangemThemePreview { + ManualBackupCompletedContent( + state = ManualBackupCompletedUM( + onContinueClick = {}, + ), + ) + } +} \ No newline at end of file From 7b862a2494678942c22b7e604bc9ccc188515b77 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 00:24:07 +0500 Subject: [PATCH 012/110] Updated on 2026-08-14 --- .../MobileWalletSetupFinishedComponent.kt | 36 ++++++ .../MobileWalletSetupFinishedModel.kt | 26 ++++ .../entity/MobileWalletSetupFinishedUM.kt | 5 + .../ui/MobileWalletSetupFinishedContent.kt | 119 ++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/entity/MobileWalletSetupFinishedUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/ui/MobileWalletSetupFinishedContent.kt diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedComponent.kt new file mode 100644 index 0000000000..21756c5932 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.hotwallet.setupfinished + +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.hotwallet.setupfinished.ui.MobileWalletSetupFinishedContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class MobileWalletSetupFinishedComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: MobileWalletSetupFinishedModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + MobileWalletSetupFinishedContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onContinueClick() + } + + data class Params( + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedModel.kt new file mode 100644 index 0000000000..40ba81c5b3 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/MobileWalletSetupFinishedModel.kt @@ -0,0 +1,26 @@ +package com.tangem.features.hotwallet.setupfinished + +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.setupfinished.entity.MobileWalletSetupFinishedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@ModelScoped +internal class MobileWalletSetupFinishedModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow( + MobileWalletSetupFinishedUM( + onContinueClick = params.callbacks::onContinueClick, + ), + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/entity/MobileWalletSetupFinishedUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/entity/MobileWalletSetupFinishedUM.kt new file mode 100644 index 0000000000..f33462eaf9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/entity/MobileWalletSetupFinishedUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet.setupfinished.entity + +internal data class MobileWalletSetupFinishedUM( + val onContinueClick: () -> Unit, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/ui/MobileWalletSetupFinishedContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/ui/MobileWalletSetupFinishedContent.kt new file mode 100644 index 0000000000..312e9b93e2 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setupfinished/ui/MobileWalletSetupFinishedContent.kt @@ -0,0 +1,119 @@ +package com.tangem.features.hotwallet.setupfinished.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.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.FullScreen +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM + +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM, modifier: Modifier = Modifier) { + val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti)) + val progress by animateLottieCompositionAsState(composition) + var showConfetti by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.ic_success_blue_76), + contentDescription = null, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 48.dp, + top = 20.dp, + end = 48.dp, + ), + text = stringResourceSafe(R.string.onboarding_done_header), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 48.dp, + top = 12.dp, + end = 48.dp, + ), + text = stringResourceSafe(R.string.backup_complete_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.weight(2f)) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + showProgress = false, + enabled = true, + onClick = state.onContinueClick, + ) + } + + if (showConfetti) { + FullScreen(notTouchable = true) { + LottieAnimation( + composition = composition, + progress = { progress }, + ) + } + } + + LaunchedEffect(Unit) { + showConfetti = true + } + + LaunchedEffect(progress == 1f) { + if (progress == 1f) { + showConfetti = false + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewMobileWalletSetupFinishedContent() { + TangemThemePreview { + MobileWalletSetupFinishedContent( + state = MobileWalletSetupFinishedUM( + onContinueClick = {}, + ), + ) + } +} \ No newline at end of file From 57cc063cdfef5ae5ad74d909499a2a9b7a5d0bd5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 12:26:39 +0500 Subject: [PATCH 013/110] Updated on 2026-08-14 --- .../features/send/v2/api/SendComponent.kt | 5 + .../send/v2/api/SendFeatureToggles.kt | 1 + .../SendDestinationBlockComponent.kt | 13 ++- .../destination/SendDestinationComponent.kt | 2 + .../send/v2/DefaultSendFeatureToggles.kt | 2 + .../features/send/v2/send/model/SendModel.kt | 10 +- .../amount/SendAmountComponent.kt | 10 +- .../amount/SendAmountReduceTrigger.kt | 8 +- .../amount/di/SendAmountModule.kt | 5 +- .../amount/model/SendAmountClickIntents.kt | 8 ++ .../amount/model/SendAmountModel.kt | 21 ++-- .../amount/ui/SendAmountContent.kt | 109 ++++++++++++++++++ .../ui/preview/SendAmountClickIntentsStub.kt | 17 +++ .../DefaultSendDestinationBlockComponent.kt | 6 +- .../DefaultSendDestinationComponent.kt | 2 +- 15 files changed, 191 insertions(+), 28 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountClickIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/preview/SendAmountClickIntentsStub.kt diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt index accb147e60..2f6034b61b 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt @@ -14,7 +14,12 @@ interface SendComponent : ComposableContentComponent { val amount: String? = null, val tag: String? = null, val destinationAddress: String? = null, + val callback: ModelCallback? = null, ) interface Factory : ComponentFactory + + interface ModelCallback { + fun onConvertToAnotherToken(lastAmount: String) + } } \ No newline at end of file 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 0e08965245..fb665394be 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,4 +3,5 @@ package com.tangem.features.send.v2.api interface SendFeatureToggles { val isSendRedesignEnabled: Boolean + val isSendWithSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt index 3e1a674323..b4468f86e4 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt @@ -1,10 +1,17 @@ package com.tangem.features.send.v2.api.subcomponents.destination -import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM interface SendDestinationBlockComponent : ComposableContentComponent { - interface Factory : - ComponentFactory + interface Factory { + fun create( + context: AppComponentContext, + params: SendDestinationComponentParams.DestinationBlockParams, + onClick: () -> Unit, + onResult: (DestinationUM) -> Unit, + ): SendDestinationBlockComponent + } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponent.kt index dff11e3f29..2c2a232547 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponent.kt @@ -7,6 +7,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat interface SendDestinationComponent : ComposableContentComponent { + fun updateState(destinationUM: DestinationUM) + interface ModelCallback : NavigationModelCallback { fun onDestinationResult(destinationUM: DestinationUM) } 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 cce9c99562..f414e52610 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,4 +8,6 @@ internal class DefaultSendFeatureToggles( ) : SendFeatureToggles { override val isSendRedesignEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") + override val isSendWithSwapEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED") } \ 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 3ffc5b25e0..5b279402b0 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 @@ -49,7 +49,7 @@ 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 import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent -import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateQRTrigger +import com.tangem.features.send.v2.subcomponents.amount.SendAmountUpdateTrigger import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -89,7 +89,7 @@ internal class SendModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, - private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger, + private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val sendFeatureToggles: SendFeatureToggles, ) : Model(), SendComponentCallback { @@ -153,6 +153,10 @@ internal class SendModel @Inject constructor( _uiState.update { sendUM } } + override fun onConvertToAnotherToken(lastAmount: String) { + params.callback?.onConvertToAnotherToken(lastAmount = lastAmount) + } + suspend fun loadFee(): Either { val predefinedValues = predefinedValues val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) { @@ -349,7 +353,7 @@ internal class SendModel @Inject constructor( ) // If it is in active state use flow to update value in amount component modelScope.launch { - amount?.let { sendAmountUpdateQRTrigger.triggerUpdateAmount(it) } + amount?.let { sendAmountUpdateTrigger.triggerUpdateAmount(it) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index bf7c0f1683..07190f20ef 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -1,19 +1,17 @@ package com.tangem.features.send.v2.subcomponents.amount -import androidx.compose.foundation.background 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.amountScreen.AmountScreenContent import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationModelCallback 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.res.TangemTheme import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel +import com.tangem.features.send.v2.subcomponents.amount.ui.SendAmountContent internal class SendAmountComponent( appComponentContext: AppComponentContext, @@ -29,15 +27,17 @@ internal class SendAmountComponent( val state by model.uiState.collectAsStateWithLifecycle() val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle() - AmountScreenContent( + SendAmountContent( amountState = state, isBalanceHidden = isBalanceHidden, clickIntents = model, - modifier = Modifier.background(TangemTheme.colors.background.tertiary), + isSendWithSwapEnabled = model.isSendWithSwapEnabled, + modifier = modifier, ) } interface ModelCallback : NavigationModelCallback { fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean) + fun onConvertToAnotherToken(lastAmount: String) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt index 459994ffcf..1bd91ddafa 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt @@ -29,7 +29,7 @@ interface SendAmountReduceListener { * Trigger amount change from another component. * Different from another triggers because it takes raw string instead of BigDecimal */ -interface SendAmountUpdateQRTrigger { +interface SendAmountUpdateTrigger { suspend fun triggerUpdateAmount(amountValue: String) } @@ -37,7 +37,7 @@ interface SendAmountUpdateQRTrigger { * Trigger amount change from another component. * Different from another triggers because it takes raw string instead of BigDecimal */ -interface SendAmountUpdateQRListener { +interface SendAmountUpdateListener { val updateAmountTriggerFlow: Flow } @@ -45,8 +45,8 @@ interface SendAmountUpdateQRListener { internal class DefaultSendAmountReduceTrigger @Inject constructor() : SendAmountReduceTrigger, SendAmountReduceListener, - SendAmountUpdateQRTrigger, - SendAmountUpdateQRListener { + SendAmountUpdateTrigger, + SendAmountUpdateListener { override val reduceToTriggerFlow = MutableSharedFlow() override val reduceByTriggerFlow = MutableSharedFlow() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt index c88ca068c2..b4c6e1b324 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.subcomponents.amount.di import com.tangem.features.send.v2.subcomponents.amount.* -import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -22,9 +21,9 @@ internal interface SendAmountModule { @Singleton @Binds - fun provideSendAmountUpdateQRListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRListener + fun provideSendAmountUpdateListener(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateListener @Singleton @Binds - fun provideSendAmountUpdateQRTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateQRTrigger + fun provideSendAmountUpdateTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountUpdateTrigger } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountClickIntents.kt new file mode 100644 index 0000000000..a97d8b6ec3 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountClickIntents.kt @@ -0,0 +1,8 @@ +package com.tangem.features.send.v2.subcomponents.amount.model + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents + +interface SendAmountClickIntents : AmountScreenClickIntents { + + fun onConvertToAnotherToken() +} \ No newline at end of file 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 383cb1f3eb..144128ac28 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 @@ -2,7 +2,6 @@ package com.tangem.features.send.v2.subcomponents.amount.model import androidx.compose.runtime.Stable import arrow.core.getOrElse -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.* import com.tangem.common.ui.amountScreen.converters.field.AmountBoundaryUpdateTransformer import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer @@ -31,7 +30,7 @@ import com.tangem.features.send.v2.api.entity.PredefinedValues 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.SendAmountUpdateQRListener +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 @@ -54,12 +53,12 @@ internal class SendAmountModel @Inject constructor( private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val sendAmountReduceListener: SendAmountReduceListener, private val feeReloadTrigger: SendFeeReloadTrigger, - private val sendAmountUpdateQRListener: SendAmountUpdateQRListener, + private val sendAmountUpdateListener: SendAmountUpdateListener, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeatureToggles: SendFeatureToggles, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, -) : Model(), AmountScreenClickIntents { +) : Model(), SendAmountClickIntents { private val params: SendAmountComponentParams = paramsContainer.require() private var appCurrency: AppCurrency = AppCurrency.Default @@ -68,6 +67,8 @@ internal class SendAmountModel @Inject constructor( private val _uiState = MutableStateFlow(params.state) val uiState = _uiState.asStateFlow() + val isSendWithSwapEnabled = sendFeatureToggles.isSendWithSwapEnabled + private val analyticsCategoryName = params.analyticsCategoryName private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = params.cryptoCurrency, @@ -99,7 +100,7 @@ internal class SendAmountModel @Inject constructor( subscribeOnAmountReduceByTriggerUpdates() subscribeOnAmountReduceToTriggerUpdates() subscribeOnAmountIgnoreReduceTriggerUpdates() - subscribeOnAmountUpdateQRTriggerUpdates() + subscribeOnAmountUpdateTriggerUpdates() } initMinBoundary() } @@ -217,6 +218,12 @@ internal class SendAmountModel @Inject constructor( saveResult() } + override fun onConvertToAnotherToken() { + val amountFieldData = uiState.value as? AmountState.Data + val amountParams = params as? SendAmountComponentParams.AmountParams + amountParams?.callback?.onConvertToAnotherToken(amountFieldData?.amountTextField?.value.orEmpty()) + } + private fun subscribeOnAmountReduceToTriggerUpdates() { sendAmountReduceListener.reduceToTriggerFlow .onEach { reduceTo -> @@ -261,8 +268,8 @@ internal class SendAmountModel @Inject constructor( .launchIn(modelScope) } - private fun subscribeOnAmountUpdateQRTriggerUpdates() { - sendAmountUpdateQRListener.updateAmountTriggerFlow + private fun subscribeOnAmountUpdateTriggerUpdates() { + sendAmountUpdateListener.updateAmountTriggerFlow .onEach { amount -> onAmountValueChange(amount) saveResult() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt new file mode 100644 index 0000000000..fd5d289b45 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.v2.subcomponents.amount.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.CircleShape +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.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.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +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.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountClickIntents +import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountClickIntentsStub + +@Composable +fun SendAmountContent( + amountState: AmountState, + isBalanceHidden: Boolean, + clickIntents: SendAmountClickIntents, + isSendWithSwapEnabled: Boolean, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) { + AmountScreenContent( + amountState = amountState, + isBalanceHidden = isBalanceHidden, + clickIntents = clickIntents, + ) + if (isSendWithSwapEnabled) { + SendConvertTokenButton( + onConvertToAnother = clickIntents::onConvertToAnotherToken, + ) + } + } +} + +@Composable +private fun SendConvertTokenButton(onConvertToAnother: () -> Unit) { + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .clickable( + indication = null, + interactionSource = null, + onClick = onConvertToAnother, + ), + ) { + Row( + modifier = Modifier + .padding(12.dp) + .align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_convert_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .size(20.dp) + .background(TangemTheme.colors.control.unchecked, CircleShape) + .padding(2.dp), + ) + Text( + text = stringResourceSafe(com.tangem.common.ui.R.string.send_amount_convert_to_another_token), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreviewProvider::class) params: AmountState) { + TangemThemePreview { + SendAmountContent( + amountState = params, + isBalanceHidden = true, + clickIntents = SendAmountClickIntentsStub, + isSendWithSwapEnabled = true, + ) + } +} + +private class SendAmountContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountStateV2, + ) +} +// endregion \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/preview/SendAmountClickIntentsStub.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/preview/SendAmountClickIntentsStub.kt new file mode 100644 index 0000000000..8c6673d5cb --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/preview/SendAmountClickIntentsStub.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.v2.subcomponents.amount.ui.preview + +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountClickIntents + +internal object SendAmountClickIntentsStub : SendAmountClickIntents { + override fun onConvertToAnotherToken() {} + + override fun onAmountValueChange(value: String) {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onMaxValueClick() {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onAmountNext() {} +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt index afd4740c34..3b51b887d7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt @@ -21,8 +21,8 @@ import kotlinx.coroutines.flow.onEach internal class DefaultSendDestinationBlockComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SendDestinationComponentParams.DestinationBlockParams, - val onClick: () -> Unit, - val onResult: (DestinationUM) -> Unit, + @Assisted val onClick: () -> Unit, + @Assisted val onResult: (DestinationUM) -> Unit, ) : SendDestinationBlockComponent, AppComponentContext by appComponentContext { private val model: SendDestinationModel = getOrCreateModel(params = params) @@ -54,6 +54,8 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor( override fun create( context: AppComponentContext, params: SendDestinationComponentParams.DestinationBlockParams, + onClick: () -> Unit, + onResult: (DestinationUM) -> Unit, ): DefaultSendDestinationBlockComponent } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationComponent.kt index aee0913947..ea22acb5bd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationComponent.kt @@ -22,7 +22,7 @@ internal class DefaultSendDestinationComponent @AssistedInject constructor( private val model: SendDestinationModel = getOrCreateModel(params = params) - fun updateState(state: DestinationUM) = model.updateState(state) + override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM) @Composable override fun Content(modifier: Modifier) { From 3aaa27ea2ef8969a5f57fa7f5ca409b8358ac062 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 13:16:23 +0400 Subject: [PATCH 014/110] Updated on 2026-08-14 --- .../components/WcConnectedAppInfoComponent.kt | 12 ++- .../WcConnectedAppInfoContainerComponent.kt | 78 +++++++++++++++++++ .../components/WcConnectionsComponent.kt | 4 +- .../model/WcConnectedAppInfoModel.kt | 17 +++- .../routes/ConnectedAppInfoRoutes.kt | 14 ++++ .../connections/ui/WcConnectionsContent.kt | 25 ++++-- .../WcSendAndReceiveBlockAidUiConverter.kt | 2 +- 7 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoContainerComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectedAppInfoRoutes.kt diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt index cfdaa99d34..0471630310 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt @@ -4,18 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.ComposableBottomSheetComponent import com.tangem.features.walletconnect.connections.model.WcConnectedAppInfoModel import com.tangem.features.walletconnect.connections.ui.WcConnectedAppInfoBS internal class WcConnectedAppInfoComponent( - params: Params, appComponentContext: AppComponentContext, + private val model: WcConnectedAppInfoModel, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { - private val model: WcConnectedAppInfoModel = getOrCreateModel(params = params) - override fun dismiss() { model.dismiss() } @@ -23,8 +20,9 @@ internal class WcConnectedAppInfoComponent( @Composable override fun BottomSheet() { val state by model.uiState.collectAsStateWithLifecycle() - state?.let { WcConnectedAppInfoBS(it) } + val appInfoUM = state + if (appInfoUM != null) { + WcConnectedAppInfoBS(appInfoUM) + } } - - data class Params(val topic: String, val onDismiss: () -> Unit) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoContainerComponent.kt new file mode 100644 index 0000000000..a522573ae4 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoContainerComponent.kt @@ -0,0 +1,78 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.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.decompose.ComposableBottomSheetComponent +import com.tangem.features.walletconnect.connections.model.WcConnectedAppInfoModel +import com.tangem.features.walletconnect.connections.routes.ConnectedAppInfoRoutes +import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory + +internal class WcConnectedAppInfoContainerComponent( + params: Params, + appComponentContext: AppComponentContext, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + private val model: WcConnectedAppInfoModel = getOrCreateModel(params = params) + private val innerRouter = InnerRouter( + stackNavigation = model.stackNavigation, + popCallback = { onChildBack() }, + ) + private val contentStack = childStack( + key = "WcConnectedAppInfoStack", + source = model.stackNavigation, + serializer = ConnectedAppInfoRoutes.serializer(), + initialConfiguration = ConnectedAppInfoRoutes.AppInfo, + handleBackButton = true, + childFactory = ::screenChild, + ) + + @Composable + override fun BottomSheet() { + val content by contentStack.subscribeAsState() + + BackHandler(onBack = ::onChildBack) + content.active.instance.BottomSheet() + } + + override fun dismiss() { + router.pop() + } + + private fun onChildBack() { + when (contentStack.value.active.configuration) { + is ConnectedAppInfoRoutes.AppInfo -> dismiss() + is ConnectedAppInfoRoutes.VerifiedAlert -> model.stackNavigation.pop() + } + } + + private fun screenChild( + config: ConnectedAppInfoRoutes, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + val appComponentContext = childByContext( + componentContext = componentContext, + router = innerRouter, + ) + return when (config) { + is ConnectedAppInfoRoutes.AppInfo -> WcConnectedAppInfoComponent( + appComponentContext = appComponentContext, + model = model, + ) + is ConnectedAppInfoRoutes.VerifiedAlert -> AlertsComponentV2( + appComponentContext = appComponentContext, + messageUM = WcAlertsFactory.createVerifiedDomainAlert(config.appName), + ) + } + } + + data class Params(val topic: String, val onDismiss: () -> Unit) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt index be19779bad..9db827960e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt @@ -54,9 +54,9 @@ internal class WcConnectionsComponent( ): ComposableBottomSheetComponent { val context = childByContext(componentContext = componentContext, messageHandler = messageHandler) return when (config) { - is WcConnectionsBottomSheetConfig.ConnectedApp -> WcConnectedAppInfoComponent( + is WcConnectionsBottomSheetConfig.ConnectedApp -> WcConnectedAppInfoContainerComponent( appComponentContext = context, - params = WcConnectedAppInfoComponent.Params( + params = WcConnectedAppInfoContainerComponent.Params( topic = config.topic, onDismiss = model.bottomSheetNavigation::dismiss, ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 507f490f0d..303e583776 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -1,10 +1,13 @@ package com.tangem.features.walletconnect.connections.model import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pushNew import com.domain.blockaid.models.dapp.CheckDAppResult 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.extensions.iconResId import com.tangem.core.ui.extensions.stringReference @@ -12,13 +15,14 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase -import com.tangem.features.walletconnect.connections.components.WcConnectedAppInfoComponent +import com.tangem.features.walletconnect.connections.components.WcConnectedAppInfoContainerComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification import com.tangem.features.walletconnect.connections.model.transformers.WcAppSubtitleConverter +import com.tangem.features.walletconnect.connections.routes.ConnectedAppInfoRoutes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -31,6 +35,7 @@ import javax.inject.Inject @Stable @ModelScoped internal class WcConnectedAppInfoModel @Inject constructor( + private val router: Router, private val wcSessionsUseCase: WcSessionsUseCase, private val messageSender: UiMessageSender, private val disconnectUseCase: WcDisconnectUseCase, @@ -38,10 +43,12 @@ internal class WcConnectedAppInfoModel @Inject constructor( paramsContainer: ParamsContainer, ) : Model() { - private val params = paramsContainer.require() + private val params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(null) + val stackNavigation = StackNavigation() + init { loadDAppInfo(params.topic) } @@ -94,7 +101,11 @@ internal class WcConnectedAppInfoModel @Inject constructor( private fun extractVerifiedState(session: WcSession): VerifiedDAppState { return if (session.securityStatus == CheckDAppResult.SAFE) { - VerifiedDAppState.Verified(onVerifiedClick = {}) + VerifiedDAppState.Verified( + onVerifiedClick = { + stackNavigation.pushNew(ConnectedAppInfoRoutes.VerifiedAlert(session.sdkModel.appMetaData.name)) + }, + ) } else { VerifiedDAppState.Unknown } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectedAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectedAppInfoRoutes.kt new file mode 100644 index 0000000000..6b327b8760 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectedAppInfoRoutes.kt @@ -0,0 +1,14 @@ +package com.tangem.features.walletconnect.connections.routes + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class ConnectedAppInfoRoutes : Route { + + @Serializable + data object AppInfo : ConnectedAppInfoRoutes() + + @Serializable + data class VerifiedAlert(val appName: String) : ConnectedAppInfoRoutes() +} \ No newline at end of file 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 f3c8baef60..a426e1026e 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -33,6 +34,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.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig @@ -206,11 +208,24 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), ) { - Text( - text = appInfo.name, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = appInfo.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + if (appInfo.verifiedState is VerifiedDAppState.Verified) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.img_approvale2_20), + contentDescription = null, + tint = Color.Unspecified, + ) + } + } Text( text = appInfo.subtitle.resolveReference(), color = TangemTheme.colors.text.tertiary, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index aa9eb45eb3..19b9ecd6d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -45,7 +45,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( is AmountInfo.FungibleTokens -> estimatedWalletChangeUMConverter.convert( WcEstimatedWalletChangeUMConverter.Input( amountInfo = it, - iconRes = R.drawable.ic_receive_new_24, + iconRes = R.drawable.ic_send_new_24, titleRes = R.string.common_send, sign = StringsSigns.MINUS, ), From 3dbaff9f06e9f456c3296437d25dab0fb54e2eae Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 11:30:21 +0500 Subject: [PATCH 015/110] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/inputrow/InputRowRecipient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8991b68840..b48f8fd849 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 @@ -127,7 +127,7 @@ fun InputRowRecipient( ) { if (isRedesignEnabled) { QrButton( - visible = !singleLine, + visible = value.isBlank(), onQrCodeClick = onQrCodeClick, ) } From cefa86bec5c02970668dde97cff2874b22603382 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 09:21:05 +0000 Subject: [PATCH 016/110] 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 b741e3bfec..5cd5b4641b 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.25.0-1100" +tangemBlockchainSdk = "develop-1104" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.25.0-486" +tangemCardSdk = "develop-487" #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 776d00eb57f4575b6b336ee756eebc7d93025bf0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 16:29:50 +0700 Subject: [PATCH 017/110] Updated on 2026-08-14 --- .../initialize/DefaultWcInitializeUseCase.kt | 4 +- .../pair/AssociateNetworksDelegate.kt | 8 +- .../pair/DefaultWcPairUseCase.kt | 2 +- .../walletconnect/pair/WcPairSdkDelegate.kt | 96 ++++++++++++++----- .../respond/DefaultWcRespondService.kt | 2 + .../sessions/DefaultWcSessionsManager.kt | 24 +++-- .../walletconnect/DefaultWcPairUseCaseTest.kt | 2 +- .../domain/walletconnect/model/WcPairError.kt | 2 +- 8 files changed, 94 insertions(+), 46 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index 1e6e533e5d..b35c96e84d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -48,7 +48,7 @@ internal class DefaultWcInitializeUseCase( application = application, metaData = appMetaData, ) { error -> - Timber.e("Error while initializing client: $error") + Timber.tag(WC_TAG).e("Error while initializing client: $error") } WalletKit.initialize( @@ -60,7 +60,7 @@ internal class DefaultWcInitializeUseCase( Timber.tag(WC_TAG).i("onWcSdkInit") }, onError = { error -> - Timber.e("Error while initializing Web3Wallet: $error") + Timber.tag(WC_TAG).e("Error while initializing Web3Wallet: $error") }, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index 808d399dab..3e01f6c9c3 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -2,6 +2,7 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet.Model.Namespace +import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -33,6 +34,8 @@ internal class AssociateNetworksDelegate( val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() + // remove duplicates + .subtract(requiredNamespaces) return userWallets .associateWith { wallet -> mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) } @@ -58,7 +61,8 @@ internal class AssociateNetworksDelegate( return@forEach } val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } - if (walletNetwork == null) { + + if (walletNetwork == null || isCustomCoin(walletNetwork)) { missingRequired.add(wcNetwork) } else { required.add(walletNetwork) @@ -68,7 +72,7 @@ internal class AssociateNetworksDelegate( val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@forEach val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } - if (walletNetwork != null) { + if (walletNetwork != null && !isCustomCoin(walletNetwork)) { available.add(walletNetwork) } else { notAdded.add(wcNetwork) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 6b60b36eca..784290a509 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -56,7 +56,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") - val error = WcPairState.Error(WcPairError.UnsupportedDomain) + val error = WcPairState.Error(WcPairError.UnsupportedDApp) emit(error) return@flow } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 0b83809241..52df62becd 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -5,59 +5,94 @@ import arrow.core.left import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.domain.walletconnect.model.WcPairError -import kotlinx.coroutines.async +import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeout +import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds internal class WcPairSdkDelegate : WcSdkObserver { private val onSessionProposal = Channel() + private val onSdkErrorCallback = Channel() private val onSessionSettleResponse = Channel() suspend fun pair(url: String): Either = coroutineScope { - suspend fun proposalCallback() = onSessionProposal - .receiveAsFlow() - .first() - + val proposalCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { proposalCallback() } } val pairCall = async { sdkPair(url) } - val proposal = async { withTimeout(20.seconds) { proposalCallback() } } pairCall.await().onLeft { - proposal.cancel() + proposalCallback.cancel() return@coroutineScope it.left() } - proposal.await().right() + proposalCallback.await() } + private suspend fun proposalCallback() = callbackFlow { + // wait first onSessionProposal callback + launch { + val sessionProposal = onSessionProposal.receiveAsFlow().first() + trySend(sessionProposal.right()) + channel.close() + } + // OR + // wait first onError callback + launch { + val error = onSdkErrorCallback.receiveAsFlow().first() + trySend(error.throwable.toPairError().left()) + channel.close() + } + awaitClose() + }.first() + suspend fun approve( sessionApprove: Wallet.Params.SessionApprove, ): Either = coroutineScope { - suspend fun approveCallback() = onSessionSettleResponse - .receiveAsFlow() - .first() - + val approveCallback = async { withTimeout(CALLBACK_TIMEOUT.seconds) { approveCallback() } } val approveCall = async { sdkApprove(sessionApprove) } - val approveCallback = async { withTimeout(20.seconds) { approveCallback() } } approveCall.await() .onLeft { approveCallback.cancel() return@coroutineScope it.left() } - when (val result = approveCallback.await()) { - is Wallet.Model.SettledSessionResponse.Result -> result.right() - is Wallet.Model.SettledSessionResponse.Error -> - WcPairError.ApprovalFailed(result.errorMessage).left() - } + return@coroutineScope approveCallback.await().fold( + ifLeft = { it.left() }, + ifRight = { result -> + when (result) { + is Wallet.Model.SettledSessionResponse.Result -> result.right() + is Wallet.Model.SettledSessionResponse.Error -> + ApprovalFailed(result.errorMessage).left() + } + }, + ) } + private suspend fun approveCallback() = callbackFlow> { + // wait first onSessionSettleResponse callback + launch { + val settledSessionResponse = onSessionSettleResponse.receiveAsFlow().first() + trySend(settledSessionResponse.right()) + channel.close() + } + // OR + // wait first onError callback + launch { + val error = onSdkErrorCallback.receiveAsFlow().first() + trySend(error.throwable.toApproveError()) + channel.close() + } + awaitClose() + }.first() + fun rejectSession(proposerPublicKey: String) { + Timber.tag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") WalletKit.rejectSession( params = Wallet.Params.SessionReject( proposerPublicKey = proposerPublicKey, @@ -68,6 +103,10 @@ internal class WcPairSdkDelegate : WcSdkObserver { ) } + override fun onError(error: Wallet.Model.Error) { + onSdkErrorCallback.trySend(error) + } + override fun onSessionProposal( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, @@ -85,8 +124,8 @@ internal class WcPairSdkDelegate : WcSdkObserver { return suspendCancellableCoroutine { continuation -> WalletKit.approveSession( params = sessionApprove, - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toApproveError()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.toApproveError()) }, ) } } @@ -95,8 +134,8 @@ internal class WcPairSdkDelegate : WcSdkObserver { return suspendCancellableCoroutine { continuation -> WalletKit.pair( params = Wallet.Params.Pair(uri), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError().left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.toPairError().left()) }, ) } } @@ -109,7 +148,12 @@ internal class WcPairSdkDelegate : WcSdkObserver { private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() companion object { + private const val CALLBACK_TIMEOUT = 30 // com.reown.android.pairing.engine.domain.PairingEngine.pair - private val pairingExpiredMessages = listOf("Pairing URI expired", "Pairing expired") + private val pairingExpiredMessages = listOf( + "Pairing URI expired", + "Pairing expired", + "No proposal or pending session authenticate request for pairing topic", + ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index 57961795e5..e87d12d41f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -24,10 +24,12 @@ internal class DefaultWcRespondService : WcRespondService { ), ), onSuccess = { + if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).i("Successful respond for request $request") continuation.resume(Unit.right()) }, onError = { + if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") continuation.resume(it.throwable.left()) }, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 66211e5d83..b82c70ec75 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -35,7 +35,6 @@ internal class DefaultWcSessionsManager( private val onSessionDelete = Channel(capacity = Channel.BUFFERED) private val oneTimeMigration = MutableStateFlow(true) - private val oneTimeSessionExtend = MutableStateFlow(true) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } @@ -50,10 +49,6 @@ internal class DefaultWcSessionsManager( val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one - if (oneTimeSessionExtend.value) { - oneTimeSessionExtend.value = false - scope.launch { extendAliveSessions(associatedSessions) } - } emit(associatedSessions.groupBy { it.wallet }) } .distinctUntilChanged() @@ -61,8 +56,8 @@ internal class DefaultWcSessionsManager( override fun onWcSdkInit() { oneTimeMigration.value = true - oneTimeSessionExtend.value = true listenOnSessionDelete() + extendSessions() } override suspend fun saveSession(session: WcSession) { @@ -154,17 +149,20 @@ internal class DefaultWcSessionsManager( return haveSomeUnknown } - private suspend fun extendAliveSessions(sessions: List) = coroutineScope { - val jobs = sessions.map { launch { sdkSessionExtend(it.sdkModel.topic) } } - jobs.joinAll() + private fun extendSessions() { + scope.launch(dispatchers.io) { + val topics: List = WalletKit.getListOfActiveSessions().map { it.topic } + val jobs = topics.map { topic -> launch { sdkSessionExtend(topic) } } + jobs.joinAll() + } } private suspend fun sdkDisconnectSession(topic: String): Either { return suspendCancellableCoroutine { continuation -> WalletKit.disconnectSession( params = Wallet.Params.SessionDisconnect(topic), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, ) } } @@ -173,8 +171,8 @@ internal class DefaultWcSessionsManager( return suspendCancellableCoroutine { continuation -> WalletKit.extendSession( params = Wallet.Params.SessionExtend(topic), - onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.left()) }, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, ) } } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 47778833c3..fecea507d0 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -196,7 +196,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `success pair and reject unsupported dApp`() = runTest { coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() - val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDomain) + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) val useCase = useCaseFactory() useCase.invoke().test { diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index 005daafedc..7d896a8539 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -8,7 +8,7 @@ sealed class WcPairError( data class UriAlreadyUsed(override val message: String) : WcPairError("107 001 001") data class PairingFailed(override val message: String) : WcPairError("107 001 002") data object InvalidDomainURL : WcPairError("107 001 003") - data object UnsupportedDomain : WcPairError("107 001 004") + data object UnsupportedDApp : WcPairError("107 001 004") data class UnsupportedBlockchains(val chains: Set) : WcPairError("107 001 005") data object InvalidConnectionRequest : WcPairError("107 002 001") From a352aea4d2a98283250f8d3e52a51e112a607678 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 14:49:17 +0500 Subject: [PATCH 018/110] Updated on 2026-08-14 --- .../qrscanning/presentation/QrScanningContent.kt | 8 ++++++-- .../qrscanning/presentation/QrScanningState.kt | 4 ++++ .../presentation/QrScanningStateController.kt | 1 + .../InitializeQrScanningStateTransformer.kt | 12 ++++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) 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 da9c43577f..b26bb96aa2 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 @@ -65,8 +65,12 @@ internal fun QrScanningContent( TangemTopAppBar( modifier = Modifier.statusBarsPadding(), - title = null, - startButton = TopAppBarButtonUM.Back(uiState.onBackClick), + title = uiState.topBarConfig.title?.resolveReference(), + startButton = TopAppBarButtonUM( + iconRes = uiState.topBarConfig.startIcon, + onIconClicked = uiState.onBackClick, + ), + textColor = TangemTheme.colors.text.constantWhite, iconTint = TangemColorPalette.White, containerColor = Color.Transparent, endContent = { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt index b18cf507af..fe2d855ea6 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt @@ -1,11 +1,13 @@ package com.tangem.feature.qrscanning.presentation +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference @Immutable internal data class QrScanningState( + val topBarConfig: TopBarConfig, val message: TextReference?, val onQrScanned: (String) -> Unit, val onBackClick: () -> Unit, @@ -14,6 +16,8 @@ internal data class QrScanningState( val bottomSheetConfig: TangemBottomSheetConfig? = null, ) +internal data class TopBarConfig(val title: TextReference?, @DrawableRes val startIcon: Int) + @Immutable internal sealed interface PasteAction { data object None : PasteAction diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt index efad8e7984..d42d54763c 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningStateController.kt @@ -26,6 +26,7 @@ internal class QrScanningStateController @Inject constructor() { private fun getInitialState(): QrScanningState { return QrScanningState( + topBarConfig = TopBarConfig(title = null, startIcon = 0), message = null, onBackClick = {}, onQrScanned = {}, diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index f4124119a7..15536a1d0b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.feature.qrscanning.impl.R import com.tangem.feature.qrscanning.model.QrScanningClickIntents import com.tangem.feature.qrscanning.presentation.PasteAction import com.tangem.feature.qrscanning.presentation.QrScanningState +import com.tangem.feature.qrscanning.presentation.TopBarConfig private const val WC_SCHEME = "wc" @@ -26,6 +27,7 @@ internal class InitializeQrScanningStateTransformer( } return QrScanningState( + topBarConfig = constructTopBarConfig(), message = message, onBackClick = clickIntents::onBackClick, onQrScanned = clickIntents::onQrScanned, @@ -34,6 +36,16 @@ internal class InitializeQrScanningStateTransformer( ) } + private fun constructTopBarConfig(): TopBarConfig { + return when (source) { + SourceType.SEND -> TopBarConfig(title = null, startIcon = R.drawable.ic_back_24) + SourceType.WALLET_CONNECT -> TopBarConfig( + title = resourceReference(R.string.wc_new_connection), + startIcon = R.drawable.ic_close_24, + ) + } + } + private fun constructPasteAction(): PasteAction { val uri = clipboardManager.getText() return if (source == SourceType.WALLET_CONNECT && uri != null && isWalletConnectUri(uri)) { From 96b522952751151985da461cd2809a0c9a9271a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 14:52:40 +0500 Subject: [PATCH 019/110] Updated on 2026-08-14 --- .../components/WcSelectWalletComponent.kt | 2 +- .../connections/ui/WcAppInfoBS.kt | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt index febecc22a5..d1db6201a3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt @@ -91,7 +91,7 @@ private fun WcSelectWalletModalBS( containerColor = TangemTheme.colors.background.primary, title = { TangemModalBottomSheetTitle( - title = stringReference("Choose networks"), + title = stringReference("Choose wallet"), startIconRes = R.drawable.ic_back_24, onStartClick = onBack, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 2cbc154342..82bcc1afc7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -260,6 +260,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier Modifier }.then(itemsModifier), walletName = state.walletName, + showEndIcon = state.onWalletClick != null, ) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( @@ -290,7 +291,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier } @Composable -private fun WalletRowItem(walletName: String, modifier: Modifier = Modifier) { +private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { Icon( modifier = Modifier.size(24.dp), @@ -315,14 +316,16 @@ private fun WalletRowItem(walletName: String, modifier: Modifier = Modifier) { style = TangemTheme.typography.body1, color = TangemTheme.colors.text.tertiary, ) - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing12) - .size(width = 18.dp, height = 24.dp), - painter = painterResource(R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + if (showEndIcon) { + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(width = 18.dp, height = 24.dp), + painter = painterResource(R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } } } @@ -597,7 +600,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Date: Mon, 7 Jul 2025 14:52:55 +0500 Subject: [PATCH 020/110] Updated on 2026-08-14 --- .../connections/components/WcConnectionsComponent.kt | 6 ++++-- .../walletconnect/connections/model/WcConnectionsModel.kt | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt index 9db827960e..8f3c586c52 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectionsComponent.kt @@ -13,6 +13,7 @@ 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.ui.DefaultUiMessageSender import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.message.EventMessageEffect @@ -27,8 +28,9 @@ internal class WcConnectionsComponent( params: Params, ) : AppComponentContext by appComponentContext, ComposableContentComponent { - private val messageHandler = EventMessageHandler() - private val model: WcConnectionsModel = getOrCreateModel(params) + private val messageHandler: EventMessageHandler = EventMessageHandler() + override val messageSender = DefaultUiMessageSender(messageHandler) + private val model: WcConnectionsModel = getOrCreateModel(params, messageSender = messageSender) private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, 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 30525785c2..7dcf11a07d 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 @@ -12,8 +12,10 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem 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.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase @@ -112,7 +114,10 @@ internal class WcConnectionsModel @Inject constructor( } private fun disconnectAllSessions() { - modelScope.launch { wcDisconnectUseCase.disconnectAll() } + modelScope.launch { + wcDisconnectUseCase.disconnectAll() + uiMessageSender.send(SnackbarMessage(message = stringReference("All dApps disconnected"))) + } } private fun getInitialState(): WcConnectionsState { From 74c09ab3bad3fbea549db455d150964b58f3da1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 12:16:22 +0400 Subject: [PATCH 021/110] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 2 +- .../com/tangem/utils/coroutines/JobHolder.kt | 3 + .../CachedCurrenciesStatusesOperations.kt | 6 +- .../wallet/child/wallet/model/WalletModel.kt | 83 +++++++++++-------- .../model/intents/WalletClickIntents.kt | 44 +++++----- .../wallet/domain/WalletContentFetcher.kt | 75 +++++++++++++++++ 6 files changed, 154 insertions(+), 59 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt 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 bbc46e5650..7608be3109 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 @@ -77,6 +77,6 @@ }, { "name": "WALLET_BALANCE_FETCHER_ENABLED", - "version": "undefined" + "version": "5.27.0" } ] diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index 4676289125..fa2f9a10b6 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -12,6 +12,9 @@ import kotlinx.coroutines.launch */ class JobHolder { + val isActive: Boolean + get() = job?.isActive ?: false + private var job: Job? = null /** Update current [JobHolder.job] and return new [job] */ 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 b6886643dc..00debb0cd3 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 @@ -85,7 +85,9 @@ class CachedCurrenciesStatusesOperations( val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull() - if (!isFetchingStarted(userWalletId) && nonEmptyCurrencies != null) { + if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId) && + nonEmptyCurrencies != null + ) { launch { setFetchStarted(userWalletId) @@ -145,7 +147,7 @@ class CachedCurrenciesStatusesOperations( ) } - if (!isFetchingStarted(userWalletId)) { + if (!tokensFeatureToggles.isWalletBalanceFetcherEnabled && !isFetchingStarted(userWalletId)) { launch { setFetchStarted(userWalletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 287c272322..27f0b11f81 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -21,7 +21,6 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency @@ -32,10 +31,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory -import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver -import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.* import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig @@ -85,7 +81,7 @@ internal class WalletModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val deepLinksRegistry: DeepLinksRegistry, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, + private val walletContentFetcher: WalletContentFetcher, private val tokensFeatureToggles: TokensFeatureToggles, private val appRouter: AppRouter, private val routingFeatureToggle: RoutingFeatureToggle, @@ -182,7 +178,7 @@ internal class WalletModel @Inject constructor( ) } .onEach(::updateWallets) - .flowOn(dispatchers.main) + .flowOn(dispatchers.default) .launchIn(modelScope) .saveIn(walletsUpdateJobHolder) } @@ -370,41 +366,43 @@ internal class WalletModel @Inject constructor( ), ) + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(action.selectedWallet) + val otherWallets = action.wallets.minus(action.selectedWallet) + + otherWallets.onEach { userWallet -> + modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { - withContext(dispatchers.io) { delay(timeMillis = 1_800) } + val direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { + Direction.RIGHT + } else { + Direction.LEFT + } - walletEventSender.send( - event = WalletEvent.DemonstrateWalletsScrollPreview( - direction = if (action.selectedWalletIndex == action.wallets.lastIndex) { - Direction.RIGHT - } else { - Direction.LEFT - }, - ), - ) + demonstrateWalletsScrollPreview(direction = direction) } } - private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { + private suspend fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { walletScreenContentLoader.cancel(action.prevWalletId) tokenListStore.remove(action.prevWalletId) + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(userWallet = action.selectedWallet) - stateHolder.update( ReinitializeWalletTransformer( prevWalletId = action.prevWalletId, @@ -415,15 +413,15 @@ internal class WalletModel @Inject constructor( ) } - private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { + private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { + fetchWalletContent(userWallet = action.selectedWallet) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, coroutineScope = modelScope, ) - fetchIfSingleWallet(userWallet = action.selectedWallet) - stateHolder.update( AddWalletTransformer( userWallet = action.selectedWallet, @@ -494,6 +492,16 @@ internal class WalletModel @Inject constructor( ) } + private fun demonstrateWalletsScrollPreview(direction: Direction) { + modelScope.launch(dispatchers.mainImmediate) { + delay(timeMillis = 1_800) + + walletEventSender.send( + event = WalletEvent.DemonstrateWalletsScrollPreview(direction = direction), + ) + } + } + private fun scrollToWallet(prevIndex: Int, newIndex: Int, onConsume: () -> Unit = {}) { // Should not show scroll animation if WalletScreen isn't in the background. if (screenLifecycleProvider.isBackgroundState.value) { @@ -521,16 +529,25 @@ internal class WalletModel @Inject constructor( } } + private suspend fun fetchWalletContent(userWallet: UserWallet) { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + /* + * Updating the balance of the current wallet is an essential part of InitializationWallets, + * so the coroutine is launched in the current context + */ + supervisorScope { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } + } else { + fetchIfSingleWallet(userWallet = userWallet) + } + } + private fun fetchIfSingleWallet(userWallet: UserWallet) { - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) - .onLeft { Timber.e(it.toString()) } - } + if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { + modelScope.launch { + fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId) + .onLeft { Timber.e(it.toString()) } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 759a4ac87b..fb53073780 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -12,13 +12,12 @@ import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -30,7 +29,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -47,7 +45,7 @@ internal class WalletClickIntents @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val fetchTokenListUseCase: FetchTokenListUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, + private val walletContentFetcher: WalletContentFetcher, private val tokensFeatureToggles: TokensFeatureToggles, private val fetchCardTokenListUseCase: FetchCardTokenListUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, @@ -92,6 +90,10 @@ internal class WalletClickIntents @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + launch { walletContentFetcher(userWalletId = it.walletId) } + } + walletScreenContentLoader.load( userWallet = it, clickIntents = this@WalletClickIntents, @@ -131,21 +133,27 @@ internal class WalletClickIntents @Inject constructor( ) modelScope.launch { - val maybeFetchResult = if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher.invoke(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .mapLeft { - Timber.e(it) - TokenListError.DataError(it) - } + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) } else { val isSingleWalletWithToken = userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isSingleWalletWithToken() - if (isSingleWalletWithToken) { + val maybeFetchResult = if (isSingleWalletWithToken) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else { fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } + + maybeFetchResult.onLeft { + stateHolder.update( + SetTokenListErrorTransformer( + selectedWallet = userWallet, + error = it, + appCurrency = getSelectedAppCurrencyUseCase.unwrap(), + ), + ) + } } buildList { @@ -159,16 +167,6 @@ internal class WalletClickIntents @Inject constructor( } .awaitAll() - maybeFetchResult.onLeft { - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = it, - appCurrency = getSelectedAppCurrencyUseCase.unwrap(), - ), - ) - } - stateHolder.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) @@ -186,11 +184,11 @@ internal class WalletClickIntents @Inject constructor( modelScope.launch { if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) + walletContentFetcher(userWalletId = userWallet.walletId, forceUpdate = true) } else { fetchCurrencyStatusUseCase(userWallet.walletId, refresh = true) } + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt new file mode 100644 index 0000000000..5ce0f3ecb1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -0,0 +1,75 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveInAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Wallet content fetcher + * + * @property walletBalanceFetcher fetcher for wallet balance + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class WalletContentFetcher @Inject constructor( + private val walletBalanceFetcher: WalletBalanceFetcher, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val fetchingJobMap = ConcurrentHashMap() + private val mutex = Mutex() + + suspend operator fun invoke(userWalletId: UserWalletId, forceUpdate: Boolean = false) = supervisorScope { + withContext(dispatchers.default) { + // Use mutex to ensure thread safety + val jobHolder = mutex.withLock { + val savedJobHolder = fetchingJobMap.getOrPut(key = userWalletId, defaultValue = ::JobHolder) + + /* + * If this is not a forced update and there is a saved job in the cache + * (doesn't matter if it is active or not), then skip the update process. + */ + if (!forceUpdate && savedJobHolder != null && !savedJobHolder.isEmpty()) { + Timber.d("Skip fetching for $userWalletId") + + return@withContext + } + + /* + * If this is a forced update and there is already a job in the cache that is updating the balance, + * then cancel the previous update. + */ + if (forceUpdate && savedJobHolder?.isActive == true) { + Timber.d("Cancel old fetching for $userWalletId") + + savedJobHolder.cancel() + } + + JobHolder().also { fetchingJobMap[userWalletId] = it } + } + + Timber.d("Start fetching for $userWalletId") + + val maybeResult = launch { + walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + .onLeft(Timber::e) + } + .saveInAndJoin(jobHolder) + + Timber.d("Finish fetching with result $maybeResult for $userWalletId") + } + } +} \ No newline at end of file From a16e7c1c650f8716d29b1500905ab9d2004de078 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 15:46:48 +0500 Subject: [PATCH 022/110] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + app/src/main/java/com/tangem/tap/common/images/Coil.kt | 2 ++ gradle/dependencies.toml | 1 + 3 files changed, 4 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 91ee07995e..cd64023632 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -299,6 +299,7 @@ dependencies { implementation(deps.zxing.qrCore) implementation(deps.coil) implementation(deps.coil.gif) + implementation(deps.coil.svg) implementation(deps.amplitude) implementation(deps.kotsonGson) implementation(deps.spongecastle.core) diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index 28336b233f..328a1d431e 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -6,6 +6,7 @@ import android.util.Log import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder +import coil.decode.SvgDecoder import coil.memory.MemoryCache import coil.request.CachePolicy import coil.util.Logger @@ -36,6 +37,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL } else { add(GifDecoder.Factory()) } + add(SvgDecoder.Factory()) } .memoryCachePolicy(CachePolicy.ENABLED) .memoryCache { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 36ba0d9d05..16f13a4a15 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -223,6 +223,7 @@ amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" } armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } +coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } From be731d07c41d217a252cea839cabc893377172b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Jul 2025 15:13:33 +0400 Subject: [PATCH 023/110] Updated on 2026-08-14 --- .../connections/ui/WcAppInfoBS.kt | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 82bcc1afc7..e1cda6ff38 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -193,11 +193,13 @@ private fun ConnectionRequestDescription(modifier: Modifier = Modifier) { iconPainter = painterResource(R.drawable.ic_check_24), tint = TangemTheme.colors.icon.accent, text = stringResourceSafe(R.string.wc_connection_reqeust_can_view_balance), + backgroundColor = TangemTheme.colors.icon.accent, ) ConnectionRequestDescriptionRow( iconPainter = painterResource(R.drawable.ic_check_24), tint = TangemTheme.colors.icon.accent, text = stringResourceSafe(R.string.wc_connection_reqeust_request_approval), + backgroundColor = TangemTheme.colors.icon.accent, ) HorizontalDivider( modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), @@ -214,6 +216,7 @@ private fun ConnectionRequestDescription(modifier: Modifier = Modifier) { iconPainter = painterResource(R.drawable.ic_close_24), tint = TangemTheme.colors.icon.warning, text = stringResourceSafe(R.string.wc_connection_reqeust_cant_sign), + backgroundColor = TangemTheme.colors.icon.warning, ) } } @@ -221,6 +224,7 @@ private fun ConnectionRequestDescription(modifier: Modifier = Modifier) { @Composable private fun ConnectionRequestDescriptionRow( iconPainter: Painter, + backgroundColor: Color, tint: Color, text: String, modifier: Modifier = Modifier, @@ -232,11 +236,20 @@ private fun ConnectionRequestDescriptionRow( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = iconPainter, - contentDescription = null, - tint = tint, + Box( + modifier = Modifier + .size(TangemTheme.dimens.size24) + .clip(CircleShape) + .background(backgroundColor.copy(alpha = 0.1F)), + contentAlignment = Alignment.Center, + content = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = iconPainter, + contentDescription = null, + tint = tint, + ) + }, ) Text( modifier = Modifier.fillMaxWidth(), From 410ecb6fc0deef4bdf6d9af24535f10ec2fd2ed5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 18:10:46 +0500 Subject: [PATCH 024/110] Updated on 2026-08-14 --- .../send/v2/api/FeeSelectorBlockComponent.kt | 10 +- .../send/v2/api/FeeSelectorComponent.kt | 10 +- .../send/v2/api/params/FeeSelectorParams.kt | 12 +- .../DefaultFeeSelectorBlockComponent.kt | 200 +++++------------- .../DefaultFeeSelectorComponent.kt | 11 +- .../v2/feeselector/model/FeeSelectorModel.kt | 21 +- .../feeselector/ui/FeeSelectorBlockContent.kt | 153 ++++++++++++++ .../ui/FeeSelectorModalBottomSheet.kt | 56 ++--- .../components/common/WcNavigationUtils.kt | 2 + .../send/WcSendTransactionComponent.kt | 3 +- .../entity/common/WcCommonTransactionModel.kt | 2 + .../transaction/model/WcAddNetworkModel.kt | 4 + .../model/WcSendTransactionModel.kt | 17 ++ .../model/WcSignTransactionModel.kt | 5 + 14 files changed, 311 insertions(+), 195 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt index 5d2744f758..dfa21df77c 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.api -import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams @@ -9,5 +9,11 @@ interface FeeSelectorBlockComponent : ComposableContentComponent { fun updateState(feeSelectorUM: FeeSelectorUM) - interface Factory : ComponentFactory + interface Factory { + fun create( + context: AppComponentContext, + params: FeeSelectorParams.FeeSelectorBlockParams, + onResult: (feeSelectorUM: FeeSelectorUM) -> Unit, + ): FeeSelectorBlockComponent + } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt index 984f411097..ff1e8703c9 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt @@ -1,9 +1,15 @@ package com.tangem.features.send.v2.api -import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.send.v2.api.params.FeeSelectorParams interface FeeSelectorComponent : ComposableBottomSheetComponent { - interface Factory : ComponentFactory + interface Factory { + fun create( + context: AppComponentContext, + params: FeeSelectorParams.FeeSelectorDetailsParams, + onDismiss: () -> Unit, + ): FeeSelectorComponent + } } \ 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 aac9799e08..db5fa2513b 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 @@ -13,27 +13,33 @@ sealed class FeeSelectorParams { abstract val state: FeeSelectorUM abstract val onLoadFee: suspend () -> Either abstract val cryptoCurrencyStatus: CryptoCurrencyStatus - abstract val callback: FeeSelectorModelCallback abstract val suggestedFeeState: SuggestedFeeState + abstract val feeDisplaySource: FeeDisplaySource data class FeeSelectorBlockParams( override val state: FeeSelectorUM, override val onLoadFee: suspend () -> Either, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - override val callback: FeeSelectorModelCallback, override val suggestedFeeState: SuggestedFeeState, + override val feeDisplaySource: FeeDisplaySource, ) : FeeSelectorParams() data class FeeSelectorDetailsParams( override val state: FeeSelectorUM, override val onLoadFee: suspend () -> Either, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - override val callback: FeeSelectorModelCallback, override val suggestedFeeState: SuggestedFeeState, + override val feeDisplaySource: FeeDisplaySource, + val callback: FeeSelectorModelCallback, ) : FeeSelectorParams() sealed class SuggestedFeeState { data object None : SuggestedFeeState() data class Suggestion(val title: TextReference, val fee: Fee) : SuggestedFeeState() } + + enum class FeeDisplaySource { + Screen, + BottomSheet, + } } \ No newline at end of file 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 8e9135b92a..0a05f94348 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 @@ -1,63 +1,65 @@ package com.tangem.features.send.v2.feeselector -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.foundation.clickable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -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.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.ui.amountScreen.utils.getFiatString +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 +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN -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.core.ui.res.TangemThemePreview -import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.core.ui.extensions.conditional import com.tangem.features.send.v2.api.FeeSelectorBlockComponent -import com.tangem.features.send.v2.api.entity.FeeFiatRateUM -import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.FeeSelectorComponent 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.impl.R +import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import java.math.BigDecimal +import kotlinx.serialization.builtins.serializer internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: FeeSelectorParams.FeeSelectorBlockParams, + @Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams, + @Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit, + private val feeSelectorComponentFactory: FeeSelectorComponent.Factory, ) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { private val model: FeeSelectorModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = model.feeSelectorBottomSheet, + serializer = Unit.serializer(), + handleBackButton = false, + childFactory = { _, componentContext -> + feeSelectorComponentFactory.create( + context = childByContext(componentContext), + params = FeeSelectorParams.FeeSelectorDetailsParams( + state = model.uiState.value, + onLoadFee = params.onLoadFee, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + callback = model, + suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + ), + onDismiss = { + model.feeSelectorBottomSheet.dismiss() + }, + ) + }, + ) + init { model.uiState - .onEach(params.callback::onFeeResult) + .onEach(onResult) .launchIn(componentScope) } @@ -68,7 +70,18 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - FeeSelectorBlockContent(modifier = modifier, state = state) + val bottomSheet by bottomSheetSlot.subscribeAsState() + + FeeSelectorBlockContent( + state = state, + modifier = modifier + .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { + Modifier.clickable { + model.feeSelectorBottomSheet.activate(Unit) + } + }, + ) + bottomSheet.child?.instance?.BottomSheet() } @AssistedFactory @@ -76,122 +89,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( override fun create( context: AppComponentContext, params: FeeSelectorParams.FeeSelectorBlockParams, + onResult: (feeSelectorUM: FeeSelectorUM) -> Unit, ): DefaultFeeSelectorBlockComponent } -} - -@Composable -private fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background(TangemTheme.colors.background.action) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(R.drawable.ic_fee_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), - text = stringResourceSafe(R.string.common_network_fee_title), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_token_info_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - SpacerWMax() - when (state) { - is FeeSelectorUM.Content -> FeeContent(state) - is FeeSelectorUM.Loading -> FeeLoading() - is FeeSelectorUM.Error -> FeeError() - } - } -} - -@Composable -private fun RowScope.FeeError() { - Text( - text = EMPTY_BALANCE_SIGN, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun RowScope.FeeLoading() { - TextShimmer( - radius = TangemTheme.dimens.radius3, - style = TangemTheme.typography.body1, - modifier = Modifier.width(width = TangemTheme.dimens.size90), - ) -} - -@Composable -private fun RowScope.FeeContent(state: FeeSelectorUM.Content) { - val fiatRate = state.feeFiatRateUM - EllipsisText( - text = if (fiatRate != null) { - getFiatString( - value = state.selectedFeeItem.fee.amount.value, - rate = fiatRate.rate, - appCurrency = fiatRate.appCurrency, - approximate = state.isFeeApproximate, - ) - } else { - state.selectedFeeItem.fee.amount.value.format { - crypto( - symbol = state.selectedFeeItem.fee.amount.currencySymbol, - decimals = state.selectedFeeItem.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) - } - }, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.End, - modifier = Modifier - .weight(1f) - .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, - ) -} - -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun FeeSelectorBlockContent_Preview() { - TangemThemePreview { - val feeItem = FeeItem.Market( - Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)), - ) - FeeSelectorBlockContent( - modifier = Modifier.fillMaxWidth(), - state = FeeSelectorUM.Content( - feeItems = persistentListOf(feeItem), - selectedFeeItem = feeItem, - isFeeApproximate = false, - feeFiatRateUM = FeeFiatRateUM( - rate = BigDecimal("2500"), - appCurrency = AppCurrency.Default, - ), - displayNonceInput = false, - nonce = null, - onNonceChange = {}, - fees = TransactionFee.Single(feeItem.fee), - ), - ) - } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index 0166fadf8f..aae3d79280 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -16,18 +16,24 @@ import dagger.assisted.AssistedInject internal class DefaultFeeSelectorComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams, + @Assisted private val onDismiss: () -> Unit, ) : FeeSelectorComponent, AppComponentContext by appComponentContext { private val model: FeeSelectorModel = getOrCreateModel(params = params) override fun dismiss() { - model.dismiss() + onDismiss() } @Composable override fun BottomSheet() { val state by model.uiState.collectAsStateWithLifecycle() - FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state, feeSelectorIntents = model) + FeeSelectorModalBottomSheet( + onDismiss = ::dismiss, + state = state, + feeSelectorIntents = model, + feeDisplaySource = params.feeDisplaySource, + ) } @AssistedFactory @@ -35,6 +41,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor( override fun create( context: AppComponentContext, params: FeeSelectorParams.FeeSelectorDetailsParams, + onDismiss: () -> Unit, ): DefaultFeeSelectorComponent } } \ No newline at end of file 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 d15ee2d65b..183224d789 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 @@ -2,14 +2,16 @@ 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.dismiss import com.tangem.blockchain.common.AmountType 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.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +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.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams @@ -30,13 +32,14 @@ internal class FeeSelectorModel @Inject constructor( paramsContainer: ParamsContainer, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val router: Router, override val dispatchers: CoroutineDispatcherProvider, -) : Model(), FeeSelectorIntents { +) : Model(), FeeSelectorIntents, FeeSelectorModelCallback { private val params = paramsContainer.require() private var appCurrency: AppCurrency = AppCurrency.Default + val feeSelectorBottomSheet = SlotNavigation() + val uiState: StateFlow field = MutableStateFlow(params.state) @@ -49,10 +52,6 @@ internal class FeeSelectorModel @Inject constructor( uiState.value = feeSelectorUM } - fun dismiss() { - router.pop() - } - private fun initAppCurrency() { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } @@ -102,7 +101,11 @@ internal class FeeSelectorModel @Inject constructor( } override fun onDoneClick() { - params.callback.onFeeResult(uiState.value) - dismiss() + (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) + } + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + uiState.value = feeSelectorUM + feeSelectorBottomSheet.dismiss() } } \ No newline at end of file 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 new file mode 100644 index 0000000000..b858c042ee --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -0,0 +1,153 @@ +package com.tangem.features.send.v2.feeselector.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.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN +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.core.ui.res.TangemThemePreview +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.api.entity.FeeFiatRateUM +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +@Composable +internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_token_info_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + SpacerWMax() + when (state) { + is FeeSelectorUM.Content -> FeeContent(state) + is FeeSelectorUM.Loading -> FeeLoading() + is FeeSelectorUM.Error -> FeeError() + } + } +} + +@Composable +private fun RowScope.FeeError() { + Text( + text = EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) +} + +@Composable +private fun RowScope.FeeLoading() { + TextShimmer( + radius = TangemTheme.dimens.radius3, + style = TangemTheme.typography.body1, + modifier = Modifier.width(width = TangemTheme.dimens.size90), + ) +} + +@Composable +private fun RowScope.FeeContent(state: FeeSelectorUM.Content) { + val fiatRate = state.feeFiatRateUM + EllipsisText( + text = if (fiatRate != null) { + getFiatString( + value = state.selectedFeeItem.fee.amount.value, + rate = fiatRate.rate, + appCurrency = fiatRate.appCurrency, + approximate = state.isFeeApproximate, + ) + } else { + state.selectedFeeItem.fee.amount.value.format { + crypto( + symbol = state.selectedFeeItem.fee.amount.currencySymbol, + decimals = state.selectedFeeItem.fee.amount.decimals, + ).fee(canBeLower = state.isFeeApproximate) + } + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + modifier = Modifier + .weight(1f) + .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, + ) +} + +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeSelectorBlockContent_Preview() { + TangemThemePreview { + val feeItem = FeeItem.Market( + Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)), + ) + FeeSelectorBlockContent( + modifier = Modifier.fillMaxWidth(), + state = FeeSelectorUM.Content( + feeItems = persistentListOf(feeItem), + selectedFeeItem = feeItem, + isFeeApproximate = false, + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("2500"), + appCurrency = AppCurrency.Default, + ), + displayNonceInput = false, + nonce = null, + onNonceChange = {}, + fees = TransactionFee.Single(feeItem.fee), + ), + ) + } +} \ No newline at end of file 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 fc27111f09..ccde03a163 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 @@ -4,10 +4,8 @@ import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.HorizontalDivider @@ -17,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.res.painterResource import androidx.compose.ui.text.input.KeyboardType @@ -52,6 +49,7 @@ import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.api.entity.FeeFiatRateUM import com.tangem.features.send.v2.api.entity.FeeItem 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.FeeSelectorIntents import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents import com.tangem.features.send.v2.impl.R @@ -65,6 +63,7 @@ import java.math.BigInteger internal fun FeeSelectorModalBottomSheet( state: FeeSelectorUM, feeSelectorIntents: FeeSelectorIntents, + feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDismiss: () -> Unit, ) { if (state !is FeeSelectorUM.Content) return @@ -77,17 +76,13 @@ internal fun FeeSelectorModalBottomSheet( ), containerColor = TangemTheme.colors.background.primary, title = { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.common_network_fee_title), - startIconRes = R.drawable.ic_back_24, - onStartClick = onDismiss, - ) + FeeTitle(feeDisplaySource = feeDisplaySource, onDismiss = onDismiss) }, content = { FeeSelectorItems( state = state, feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp), + modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp), ) }, footer = { @@ -102,6 +97,26 @@ internal fun FeeSelectorModalBottomSheet( ) } +@Composable +private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDismiss: () -> Unit) { + when (feeDisplaySource) { + FeeSelectorParams.FeeDisplaySource.Screen -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.common_network_fee_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onDismiss, + ) + } + FeeSelectorParams.FeeDisplaySource.BottomSheet -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.common_network_fee_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = onDismiss, + ) + } + } +} + @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( @@ -129,21 +144,7 @@ private fun FeeSelectorItems( val itemModifier = Modifier .fillMaxWidth() .background(TangemTheme.colors.background.primary) - .then( - if (isSelected) { - Modifier - .border( - width = 2.5.dp, - color = iconTint.copy(alpha = 0.2F), - shape = RoundedCornerShape(16.dp), - ) - .padding(2.5.dp) - .border(width = 1.dp, color = iconTint, shape = RoundedCornerShape(14.dp)) - .clip(RoundedCornerShape(14.dp)) - } else { - Modifier - }, - ) + .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { is FeeItem.Suggested -> RegularFeeItemContent( @@ -477,7 +478,12 @@ private fun FeeSelectorBS_Preview( state: FeeSelectorUM.Content, ) { TangemThemePreview { - FeeSelectorModalBottomSheet(onDismiss = {}, state = state, feeSelectorIntents = StubFeeSelectorIntents()) + FeeSelectorModalBottomSheet( + onDismiss = {}, + state = state, + feeSelectorIntents = StubFeeSelectorIntents(), + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, + ) } } 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 7bbe97cb99..7089bc96f9 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 @@ -41,12 +41,14 @@ internal fun getWcCommonScreen( val state = model.uiState.value ?: error("in this step state should be not null") feeSelectorComponentFactory.create( context = appComponentContext, + onDismiss = model::popBack, params = FeeSelectorParams.FeeSelectorDetailsParams( state = state.feeSelectorUM, onLoadFee = model::loadFee, cryptoCurrencyStatus = model.cryptoCurrencyStatus, callback = model, suggestedFeeState = model.suggestedFeeState, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, ), ) } 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 b7e2f9a519..26b445e05c 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 @@ -26,9 +26,10 @@ internal class WcSendTransactionComponent( state = state.feeSelectorUM, onLoadFee = model::loadFee, cryptoCurrencyStatus = model.cryptoCurrencyStatus, - callback = model, suggestedFeeState = model.suggestedFeeState, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, ), + onResult = model::updateFee, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt index bf422a8b13..c8afce4c4a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/common/WcCommonTransactionModel.kt @@ -7,4 +7,6 @@ internal interface WcCommonTransactionModel { val uiState: StateFlow fun dismiss() + + fun popBack() } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 2ca9466415..911d108202 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -67,6 +67,10 @@ internal class WcAddNetworkModel @Inject constructor( _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } + override fun popBack() { + router.pop() + } + fun showTransactionRequest() { stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } 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 7e2951dbb6..db845051ad 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 @@ -118,7 +118,20 @@ internal class WcSendTransactionModel @Inject constructor( } } + /** + * Handles callback with updated [feeSelectorUM] from click FeeSelectorComponent. + * We need to trigger navigation to dismiss fee selector component + */ override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + updateFee(feeSelectorUM) + popBack() + } + + /** + * Handles fee updates. + * Also handles fee results from FeeSelectorBlockComponent + */ + fun updateFee(feeSelectorUM: FeeSelectorUM) { _uiState.update { it?.copy(feeSelectorUM = feeSelectorUM) } val fee = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee ?: return (useCase as? WcMutableFee)?.updateFee(fee) @@ -186,6 +199,10 @@ internal class WcSendTransactionModel @Inject constructor( _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } + override fun popBack() { + stackNavigation.pop() + } + fun showTransactionRequest() { stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index f61e0b2545..34252a2a2e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.model import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -76,6 +77,10 @@ internal class WcSignTransactionModel @Inject constructor( _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } + override fun popBack() { + stackNavigation.pop() + } + fun showTransactionRequest() { stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } From 9ad097686596db3dd82f0d27c2d8192d65651aa0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Jul 2025 18:16:10 +0500 Subject: [PATCH 025/110] Updated on 2026-08-14 --- .../phrase/ManualBackupPhraseComponent.kt | 37 ++++ .../phrase/entity/ManualBackupPhraseUM.kt | 14 ++ .../phrase/model/ManualBackupPhraseModel.kt | 32 ++++ .../phrase/ui/ManualBackupPhraseContent.kt | 181 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt 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 new file mode 100644 index 0000000000..1dd8b360fc --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ManualBackupPhraseComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.hotwallet.manualbackup.phrase + +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.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel +import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class ManualBackupPhraseComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + private val model: ManualBackupPhraseModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ManualBackupPhraseContent( + state = state, + modifier = modifier, + ) + } + + interface ModelCallbacks { + fun onContinueClick() + } + + data class Params( + 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/entity/ManualBackupPhraseUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt new file mode 100644 index 0000000000..3a799f4f31 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/entity/ManualBackupPhraseUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet.manualbackup.phrase.entity + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal data class ManualBackupPhraseUM( + val onContinueClick: () -> Unit, + val words: ImmutableList = persistentListOf(), +) { + data class MnemonicGridItem( + val index: Int, + val mnemonic: String, + ) +} \ 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 new file mode 100644 index 0000000000..4320a86026 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -0,0 +1,32 @@ +package com.tangem.features.hotwallet.manualbackup.phrase.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.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class ManualBackupPhraseModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + private val callbacks = params.callbacks + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialUIState()) + + private fun getInitialUIState(): ManualBackupPhraseUM { + return ManualBackupPhraseUM( + onContinueClick = callbacks::onContinueClick, + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt new file mode 100644 index 0000000000..3bba1be463 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/ui/ManualBackupPhraseContent.kt @@ -0,0 +1,181 @@ +package com.tangem.features.hotwallet.manualbackup.phrase.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.platform.LocalLayoutDirection +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.LayoutDirection +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.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) { + Column( + modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize(), + ) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .imePadding() + .weight(1f), + ) { + TitleBlock( + state = state, + modifier = Modifier.padding(top = 20.dp), + ) + + SeedPhraseGridBlock( + mnemonicGridItems = state.words, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 32.dp), + ) + } + + Text( + text = stringResourceSafe(R.string.backup_seed_responsibility), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + text = stringResourceSafe(id = R.string.common_continue), + onClick = state.onContinueClick, + ) + } +} + +@Composable +private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(horizontal = TangemTheme.dimens.size36) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = stringResourceSafe(R.string.backup_seed_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResourceSafe( + R.string.backup_seed_description, + state.words.size, + ), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun SeedPhraseGridBlock( + mnemonicGridItems: ImmutableList, + modifier: Modifier = Modifier, +) { + VerticalGrid( + modifier = modifier, + items = mnemonicGridItems, + ) { item -> + Row( + modifier = Modifier.padding(all = TangemTheme.dimens.size8), + verticalAlignment = Alignment.CenterVertically, + ) { + if (LocalLayoutDirection.current == LayoutDirection.Ltr) { + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + } else { + Text( + text = item.mnemonic, + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.width(TangemTheme.dimens.size40), + text = "${item.index}.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +@Composable +private inline fun VerticalGrid( + items: ImmutableList, + modifier: Modifier = Modifier, + crossinline content: @Composable (T) -> Unit, +) { + val columnLength = items.size / 2 + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + repeat(2) { index -> + Column { + for (i in 0 until columnLength) { + val item = items[index * columnLength + i] + content(item) + } + } + } + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + ManualBackupPhraseContent( + state = ManualBackupPhraseUM( + onContinueClick = {}, + words = List(12) { + ManualBackupPhraseUM.MnemonicGridItem( + index = it + 1, + mnemonic = "word${it + 1}", + ) + }.toImmutableList(), + ), + ) + } +} \ No newline at end of file From cd9cf7abfd606b0063773d6c0cbce6e54a499a70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Jul 2025 11:13:46 +0500 Subject: [PATCH 026/110] Updated on 2026-08-14 --- .../send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt | 7 +++++++ .../send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt | 2 ++ 2 files changed, 9 insertions(+) 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 ec215b0478..de2ae7cbb4 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 @@ -2,8 +2,11 @@ package com.tangem.features.send.v2.sendnft.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.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 /** @@ -18,11 +21,15 @@ internal sealed class NFTSendAnalyticEvents( data class TransactionScreenOpened( val token: String, val feeType: AnalyticsParam.FeeType, + val blockchain: String, + val nonceNotEmpty: Boolean, ) : NFTSendAnalyticEvents( event = "NFT Sent Screen Opened", params = mapOf( TOKEN_PARAM to token, FEE_TYPE to feeType.value, + BLOCKCHAIN to blockchain, + NONCE to nonceNotEmpty.toString().capitalize(), ), ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index f19c77aec4..b9e53dda49 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -26,6 +26,8 @@ internal class NFTSendAnalyticHelper @Inject constructor( NFTSendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, feeType = feeType, + blockchain = cryptoCurrency.network.name, + nonceNotEmpty = feeSelectorUM.nonce != null, ), ) analyticsEventHandler.send( From 01d7f7221ee16960b65a33f2f29d404edccd6c1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Jul 2025 13:59:26 +0500 Subject: [PATCH 027/110] Updated on 2026-08-14 --- .../v2/subcomponents/destination/model/SendDestinationModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2d3ee4b04c..0cc9a81903 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 @@ -347,7 +347,7 @@ internal class SendDestinationModel @Inject constructor( params.onNextClick() }, ), - prevButton = if (!route.isEditMode && isRedesignEnabled) { + prevButton = if (!route.isEditMode && !isRedesignEnabled) { NavigationButton( textReference = TextReference.EMPTY, iconRes = R.drawable.ic_back_24, From 48180a9b38f1792d3fea632292486576153108d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Jul 2025 13:30:51 +0500 Subject: [PATCH 028/110] Updated on 2026-08-14 --- .../features/send/v2/send/model/SendModel.kt | 6 ++++++ .../subcomponents/amount/SendAmountComponent.kt | 2 ++ .../amount/model/SendAmountModel.kt | 17 +++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) 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 5b279402b0..96a985f08d 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 @@ -43,6 +43,7 @@ import com.tangem.features.send.v2.api.entity.PredefinedValues 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 +import com.tangem.domain.wallets.models.GetUserWalletError 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.confirm.SendConfirmComponent @@ -157,6 +158,11 @@ internal class SendModel @Inject constructor( params.callback?.onConvertToAnotherToken(lastAmount = lastAmount) } + override fun onError(error: GetUserWalletError) { + Timber.w(error.toString()) + showAlertError() + } + suspend fun loadFee(): Either { val predefinedValues = predefinedValues val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index 07190f20ef..cebf431139 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.navigationButtons.NavigationModelCallback 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.wallets.models.GetUserWalletError import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.amount.ui.SendAmountContent @@ -39,5 +40,6 @@ internal class SendAmountComponent( interface ModelCallback : NavigationModelCallback { fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean) fun onConvertToAnotherToken(lastAmount: String) + fun onError(error: GetUserWalletError) } } \ No newline at end of file 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 144128ac28..16ba4a5cef 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 @@ -85,10 +85,19 @@ internal class SendAmountModel @Inject constructor( } private fun initAppCurrency() { - modelScope.launch { - userWallet = getUserWalletUseCase(params.userWalletId).getOrNull() - appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } - } + getUserWalletUseCase.invokeFlow(params.userWalletId) + .onEach { either -> + either.fold( + ifLeft = { error -> + val amountParams = params as? SendAmountComponentParams.AmountParams + amountParams?.callback?.onError(error) + }, + ifRight = { wallet -> + userWallet = wallet + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + }, + ) + }.launchIn(modelScope) } private fun subscribeOnCryptoCurrencyStatusFlow() { From 76164ac486e296b598b9eb148726ad99df15c4b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Jul 2025 15:42:29 +0500 Subject: [PATCH 029/110] Updated on 2026-08-14 --- .../tap/routing/utils/DeepLinkFactoryTest.kt | 12 +- .../com/tangem/utils/extensions/StringExt.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 1 + .../DefaultTokenDetailsDeepLinkHandler.kt | 135 +++++++++++++----- .../TokenDetailsDeepLinkActionTrigger.kt | 27 ++++ .../deeplink/di/TokenDetailsDeepLinkModule.kt | 15 ++ .../tokendetails/model/TokenDetailsModel.kt | 19 ++- .../deeplink/SelectWalletInDeepLinkTrigger.kt | 12 ++ .../wallet/child/wallet/model/WalletModel.kt | 36 ++++- .../DefaultWalletDeepLinkActionTrigger.kt | 24 ++++ .../deeplink/di/WalletDeepLinkModule.kt | 11 ++ .../wallet/state/WalletStateController.kt | 5 + 12 files changed, 249 insertions(+), 50 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/TokenDetailsDeepLinkActionTrigger.kt create mode 100644 features/wallet/api/src/main/kotlin/com/tangem/features/wallet/deeplink/SelectWalletInDeepLinkTrigger.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultWalletDeepLinkActionTrigger.kt diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 018f3b76b8..54c507c959 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -348,11 +348,10 @@ class DeepLinkFactoryTest { fun `getParams filters malicious parameters`() = runTest { every { mockedUri.scheme } returns "tangem" every { mockedUri.host } returns "onramp" - every { mockedUri.query } returns "safe=ok&malicious=%3Cscript%3E"e=O%27Brien" - every { mockedUri.queryParameterNames } returns setOf("safe", "malicious", "quote") + every { mockedUri.query } returns "safe=ok&malicious=%3Cscript%3E" + every { mockedUri.queryParameterNames } returns setOf("safe", "malicious") every { mockedUri.getQueryParameter("safe") } returns "ok" every { mockedUri.getQueryParameter("malicious") } returns "