From b8f3d5315ff55f1ceb2c0e0c6918e98d014d696c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:11:54 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../impl/DefaultMarketsPortfolioComponent.kt | 29 +++++ .../impl/model/MarketsPortfolioModel.kt | 74 ++++++++++- .../nft/common/DefaultNFTComponent.kt | 3 + .../nft/receive/NFTReceiveComponent.kt | 29 +++++ .../nft/receive/model/NFTReceiveModel.kt | 95 +++++++++++++-- .../impl/DefaultOnboardingNoteComponent.kt | 3 + .../topup/OnboardingNoteTopUpComponent.kt | 29 +++++ .../topup/model/OnboardingNoteTopUpModel.kt | 70 ++++++++++- .../impl/DefaultOnboardingTwinComponent.kt | 29 +++++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 115 ++++++++++++++---- .../component/DefaultTokenReceiveComponent.kt | 69 +---------- .../model/TokenReceiveQrCodeModel.kt | 4 +- .../tokenreceive/ui/TokenReceiveContent.kt | 65 ++++++++++ .../tokenreceive/ui/state/QrCodeUM.kt | 2 +- .../wallet/child/wallet/WalletComponent.kt | 12 ++ .../WalletCurrencyActionsClickIntents.kt | 72 +++++++++-- .../router/DefaultWalletRouter.kt | 8 ++ .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/WalletStateController.kt | 10 ++ .../wallet/state/model/WalletDialogConfig.kt | 4 + 20 files changed, 607 insertions(+), 118 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 73841ec05d..8f3e1d6da7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -5,12 +5,20 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -19,20 +27,41 @@ import dagger.assisted.AssistedInject internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: MarketsPortfolioComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : AppComponentContext by context, MarketsPortfolioComponent { private val model: MarketsPortfolioModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + override fun setTokenNetworks(networks: List) = model.setTokenNetworks(networks) override fun setNoNetworksAvailable() = model.setNoNetworksAvailable() @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() MyPortfolio(modifier = modifier, state = state) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + @AssistedFactory interface Factory : MarketsPortfolioComponent.Factory { override fun create( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index e17c925595..60673724c0 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.model import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -15,24 +17,31 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.ArtworkModel +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM +import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,7 +52,7 @@ import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class MarketsPortfolioModel @Inject constructor( @@ -60,6 +69,9 @@ internal class MarketsPortfolioModel @Inject constructor( private val getCardImageUseCase: GetCardImageUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { val state: StateFlow get() = _state @@ -80,6 +92,8 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val currentAppCurrency = getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } @@ -128,7 +142,11 @@ internal class MarketsPortfolioModel @Inject constructor( tokenActionsHandler = tokenActionsIntentsFactory.create( currentAppCurrency = Provider { currentAppCurrency.value }, updateTokenReceiveBSConfig = { updateBlock -> - updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) { + updateTokensState { + it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) + } + } }, onHandleQuickAction = { handledAction -> analyticsEventHandler.send( @@ -137,6 +155,9 @@ internal class MarketsPortfolioModel @Inject constructor( blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name, ), ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + configureReceiveAddresses(handledAction) + } }, ), updateTokens = { updateBlock -> @@ -359,4 +380,51 @@ internal class MarketsPortfolioModel @Inject constructor( block(tokensState) } } + + private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { + when (quickAction.action) { + TokenActionsBSContentUM.Action.Receive -> { + val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return + val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency + modelScope.launch { + val ensName = getEnsNameUseCase.invoke( + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + network = cryptoCurrency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})", + ), + ) + } + } + val tokenConfig = TokenReceiveConfig( + shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, + showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + bottomSheetNavigation.activate(tokenConfig) + } + } + else -> Unit + } + } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 5c4874dd5e..6aa2bc4a8f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -22,6 +22,7 @@ import com.tangem.features.nft.details.info.NFTDetailsInfoComponent import com.tangem.features.nft.entity.NFTSendSuccessListener import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.traits.NFTAssetTraitsComponent +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -35,6 +36,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( @Assisted private val params: NFTComponent.Params, private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory, nftSendSuccessListener: NFTSendSuccessListener, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( walletName = params.walletName, onBackClick = ::onChildBack, ), + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) private fun getDetailsComponent( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index f445c95fb7..e4bd9a112e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -4,29 +4,58 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.receive.ui.NFTReceive +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedInject internal class NFTReceiveComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by context { private val model: NFTReceiveModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() NFTReceive(state, modifier) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class Params( val userWalletId: UserWalletId, val walletName: String, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index fc2df6f5c2..632959d7e8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.nft.receive.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -12,12 +14,20 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.Asset +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.TokenReceiveNotification import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTCurrencyUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.nft.GetNFTNetworksUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.NFTReceiveComponent import com.tangem.features.nft.receive.entity.NFTReceiveUM @@ -25,6 +35,8 @@ import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheet import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* @@ -43,11 +55,17 @@ internal class NFTReceiveModel @Inject constructor( private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, private val messageSender: UiMessageSender, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, + private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase, paramsContainer: ParamsContainer, ) : Model() { private val params: NFTReceiveComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _state = MutableStateFlow( value = NFTReceiveUM( onBackClick = params.onBackClick, @@ -138,14 +156,23 @@ internal class NFTReceiveModel @Inject constructor( when (val value = networkStatus.value) { is NetworkStatus.Verified -> { - _state.update { - ShowReceiveBottomSheetTransformer( - network = network, - networkAddress = value.address, - onDismissBottomSheet = ::onReceiveBottomSheetDismiss, - onCopyClick = { text -> onCopyClick(text, network) }, - onShareClick = { text -> onShareClick(text, network) }, - ).transform(it) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + bottomSheetNavigation.activate( + configuration = configureReceiveAddresses( + addresses = value.address, + network = network, + ), + ) + } else { + _state.update { + ShowReceiveBottomSheetTransformer( + network = network, + networkAddress = value.address, + onDismissBottomSheet = ::onReceiveBottomSheetDismiss, + onCopyClick = { text -> onCopyClick(text, network) }, + onShareClick = { text -> onShareClick(text, network) }, + ).transform(it) + } } } is NetworkStatus.MissedDerivation, @@ -166,4 +193,56 @@ internal class NFTReceiveModel @Inject constructor( analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name)) shareManager.shareText(text = text) } + + private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig { + val cryptoCurrency = getNFTCurrencyUseCase.invoke(network) + + val ensName = getEnsNameUseCase.invoke( + userWalletId = params.userWalletId, + network = network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = cryptoCurrency.symbol, + ), + ) + } + } + + val notifications = buildList { + if (BlockchainUtils.isSolana(network.rawId)) { + add( + TokenReceiveNotification( + title = R.string.nft_receive_unsupported_types, + subtitle = R.string.nft_receive_unsupported_types_description, + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrency, + userWalletId = params.userWalletId, + showMemoDisclaimer = false, + receiveAddress = receiveAddresses, + tokenReceiveNotification = notifications, + asset = Asset.NFT, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index bea41ef278..eab88630a0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -28,6 +28,7 @@ import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -39,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -108,6 +110,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onDone = { params.onDone() }, ), + tokenReceiveComponentFactory = tokenReceiveComponentFactory, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt index 5b9cc4f1af..771e29c209 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt @@ -6,23 +6,40 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp +import com.tangem.features.tokenreceive.TokenReceiveComponent internal class OnboardingNoteTopUpComponent( appComponentContext: AppComponentContext, private val params: Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) @@ -30,8 +47,20 @@ internal class OnboardingNoteTopUpComponent( modifier = modifier, state = state, ) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class Params( val childParams: DefaultOnboardingNoteComponent.ChildParams, val onDone: () -> Unit, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 4f0a65a810..a8e760dbf3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped @@ -13,23 +15,29 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isPositive import kotlinx.coroutines.flow.* @@ -54,6 +62,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { private val params = paramsContainer.require() @@ -61,6 +72,8 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val scanResponse = params.childParams.commonState.value.scanResponse private var userWallet = params.childParams.commonState.value.userWallet + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _uiState = MutableStateFlow( OnboardingNoteTopUpUM( onRefreshBalanceClick = ::refreshBalance, @@ -112,8 +125,18 @@ internal class OnboardingNoteTopUpModel @Inject constructor( val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return val networkAddress = currencyStatus.value.networkAddress ?: return - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + val userWalletId = userWallet?.walletId ?: return + modelScope.launch { + configureReceiveAddresses( + cryptoCurrencyStatus = currencyStatus, + userWalletId = userWalletId, + )?.let { bottomSheetNavigation.activate(it) } + } + } else { + _uiState.update { + it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + } } Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) } @@ -250,4 +273,47 @@ internal class OnboardingNoteTopUpModel @Inject constructor( saveWalletUseCase(wallet, false) return wallet } + + private suspend fun configureReceiveAddresses( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWalletId: UserWalletId, + ): TokenReceiveConfig? { + val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index ded3238c83..0c4797f1a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -5,16 +5,24 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder import com.tangem.core.decompose.navigation.inner.InnerNavigationState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -23,10 +31,18 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnboardingTwinComponent.Params, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : OnboardingTwinComponent, AppComponentContext by appComponentContext, InnerNavigationHolder { private val model: OnboardingTwinModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TokenReceiveConfig.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + init { params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) } @@ -43,14 +59,27 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { BackHandler { model.onBack() } + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by model.uiState.collectAsStateWithLifecycle() OnboardingTwin( state = state, modifier = modifier, ) + bottomSheet.child?.instance?.BottomSheet() } + private fun bottomSheetChild( + config: TokenReceiveConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = config, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + data class TwinInnerNavigationState( override val stackSize: Int, ) : InnerNavigationState { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 2e3a94788a..f0076b3bee 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -30,6 +32,8 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse @@ -38,9 +42,11 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -52,6 +58,7 @@ import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -89,12 +96,17 @@ internal class OnboardingTwinModel @Inject constructor( private val shareManager: ShareManager, private val tokensFeatureToggles: TokensFeatureToggles, private val walletBalanceFetcher: WalletBalanceFetcher, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : Model() { private val params = paramsContainer.require() private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") private val cryptoCurrencyStatusJobHolder = JobHolder() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val _uiState = MutableStateFlow( when (params.mode) { Mode.WelcomeOnly -> { @@ -400,35 +412,43 @@ internal class OnboardingTwinModel @Inject constructor( val currency = status.currency val networkAddress = status.value.networkAddress ?: return - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + modelScope.launch { + configureReceiveAddresses(cryptoCurrencyStatus = status)?.let { + bottomSheetNavigation.activate(it) + } + } + } else { + update { + it.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + update { + it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) + } + }, + content = TokenReceiveBottomSheetConfig( + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, + showMemoDisclaimer = + currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + shareManager.shareText(text = it) + }, ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, ), - ), - ) + ) + } } } @@ -506,4 +526,45 @@ internal class OnboardingTwinModel @Inject constructor( TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard } + + private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { + val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: return null + val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + address = addresses.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWallet.walletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index 291ad7b029..e467be69ac 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -2,28 +2,19 @@ package com.tangem.features.tokenreceive.component import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.tokenreceive.model.TokenReceiveModel import com.tangem.features.tokenreceive.route.TokenReceiveRoutes -import com.tangem.features.tokenreceive.ui.TokenReceiveContent +import com.tangem.features.tokenreceive.ui.TokenReceiveContentSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -133,64 +124,6 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( } } -@Composable -internal fun TokenReceiveContentSheet( - route: TokenReceiveRoutes, - onCloseClick: () -> Unit, - onBackClick: () -> Unit, - contentStack: ChildStack, -) { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onCloseClick, - content = TangemBottomSheetConfigContent.Empty, - ), - onBack = onBackClick, - containerColor = TangemTheme.colors.background.tertiary, - title = { - Title( - route = route, - onBackClick = onBackClick, - onCloseClick = onCloseClick, - ) - }, - content = { - TokenReceiveContent( - stackState = contentStack, - modifier = Modifier, - ) - }, - ) -} - -@Composable -private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { - when (route) { - is TokenReceiveRoutes.QrCode -> { - TangemModalBottomSheetTitle( - startIconRes = R.drawable.ic_back_24, - onStartClick = onBackClick, - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - TokenReceiveRoutes.ReceiveAssets -> { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.domain_receive_assets_navigation_title), - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - TokenReceiveRoutes.Warning -> { - TangemModalBottomSheetTitle( - endIconRes = R.drawable.ic_close_24, - onEndClick = onCloseClick, - ) - } - } -} - internal interface TokenReceiveModelCallback : TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback, TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback, diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt index e7b1915b1b..68246b663f 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/model/TokenReceiveQrCodeModel.kt @@ -4,7 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent import com.tangem.features.tokenreceive.entity.ReceiveAddress import com.tangem.features.tokenreceive.ui.state.QrCodeUM @@ -27,7 +27,7 @@ internal class TokenReceiveQrCodeModel @Inject constructor( QrCodeUM( network = params.network, addressValue = params.address.value, - addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: stringReference(""), + addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY, onCopyClick = { params.callback.onCopyClick(params.id) }, onShareClick = params.callback::onShareClick, ), diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt index 5571b27657..75ee1fb93f 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt @@ -8,9 +8,74 @@ import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.fade import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokenreceive.route.TokenReceiveRoutes +@Composable +internal fun TokenReceiveContentSheet( + route: TokenReceiveRoutes, + onCloseClick: () -> Unit, + onBackClick: () -> Unit, + contentStack: ChildStack, +) { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onCloseClick, + content = TangemBottomSheetConfigContent.Empty, + ), + onBack = onBackClick, + containerColor = TangemTheme.colors.background.tertiary, + title = { + Title( + route = route, + onBackClick = onBackClick, + onCloseClick = onCloseClick, + ) + }, + content = { + TokenReceiveContent( + stackState = contentStack, + modifier = Modifier, + ) + }, + ) +} + +@Composable +private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) { + when (route) { + is TokenReceiveRoutes.QrCode -> { + TangemModalBottomSheetTitle( + startIconRes = R.drawable.ic_back_24, + onStartClick = onBackClick, + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.ReceiveAssets -> { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.domain_receive_assets_navigation_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + TokenReceiveRoutes.Warning -> { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = onCloseClick, + ) + } + } +} + @Composable internal fun TokenReceiveContent( stackState: ChildStack, diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt index 274735da2a..f9425a4fc6 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.tokenreceive.ui.state import com.tangem.core.ui.extensions.TextReference internal data class QrCodeUM( - val onCopyClick: (Int) -> Unit, + val onCopyClick: () -> Unit, val onShareClick: (String) -> Unit, val addressName: TextReference, val addressValue: String, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 07a629a880..94c51ca420 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -23,11 +23,13 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, @@ -35,6 +37,7 @@ internal class WalletComponent @AssistedInject constructor( private val marketsEntryComponentFactory: MarketsEntryComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, + private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -76,6 +79,15 @@ internal class WalletComponent @AssistedInject constructor( modelCallbacks = model.askForPushNotificationsModelCallbacks, ), ) + is WalletDialogConfig.TokenReceive -> { + tokenReceiveComponentFactory.create( + context = childByContext(componentContext), + params = TokenReceiveComponent.Params( + config = dialogConfig.tokenReceiveConfig, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 29c08244a6..71d5e76c5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -29,6 +29,8 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.ReceiveAddressModel +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -41,6 +43,7 @@ import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -49,6 +52,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText +import com.tangem.domain.transaction.usecase.GetEnsNameUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -61,6 +65,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -128,6 +133,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val shareManager: ShareManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, + private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle, + private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getEnsNameUseCase: GetEnsNameUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -172,13 +180,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event?.let { analyticsEventHandler.send(it) } - stateHolder.showBottomSheet( - createReceiveBottomSheetContent( - currency = cryptoCurrencyStatus.currency, - addresses = cryptoCurrencyStatus.value.networkAddress ?: return, - ), - userWalletId, - ) + if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) { + stateHolder.hideBottomSheet() + modelScope.launch { + configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let { + router.openTokenReceiveBottomSheet(it) + } + } + } else { + stateHolder.showBottomSheet( + createReceiveBottomSheetContent( + currency = cryptoCurrencyStatus.currency, + addresses = cryptoCurrencyStatus.value.networkAddress ?: return, + ), + userWalletId, + ) + } } override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? { @@ -639,4 +656,45 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( targetRoute } } + + private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? { + val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null + val userWalletId = stateHolder.getSelectedWalletId() + + val ensName = getEnsNameUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + address = networkAddress.defaultAddress.value, + ) + + val receiveAddresses = buildList { + ensName?.let { ens -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Ens, + value = ens, + displayName = ens, + ), + ) + } + networkAddress.availableAddresses.map { address -> + add( + ReceiveAddressModel( + nameService = ReceiveAddressModel.NameService.Default, + value = address.value, + displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})", + ), + ) + } + } + + return TokenReceiveConfig( + shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(), + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = userWalletId, + showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network + .TransactionExtrasType.NONE, + receiveAddress = receiveAddresses, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 8c39cfc9e5..602de14421 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,11 +1,13 @@ package com.tangem.feature.wallet.presentation.router import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -98,4 +100,10 @@ internal class DefaultWalletRouter @Inject constructor( ), ) } + + override fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) { + dialogNavigation.activate( + configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index b1670c0110..946a3e7692 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -54,4 +55,6 @@ internal interface InnerWalletRouter { /** Open NFT collections screen */ fun openNFT(userWallet: UserWallet) + + fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 1cb651abd1..de8c2ee08a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -90,6 +90,16 @@ internal class WalletStateController @Inject constructor() { ) } + fun hideBottomSheet() { + update( + transformer = OpenBottomSheetTransformer( + userWalletId = getSelectedWalletId(), + content = TangemBottomSheetConfigContent.Empty, + onDismissBottomSheet = {}, + ), + ) + } + private fun getInitialState(): WalletScreenState { return WalletScreenState( topBarConfig = WalletTopBarConfig(onDetailsClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index 2134c40ef5..9bce963c5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.model +import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -19,4 +20,7 @@ internal sealed interface WalletDialogConfig { @Serializable data object AskForPushNotifications : WalletDialogConfig + + @Serializable + data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig } \ No newline at end of file