Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-22 12:07:24 +03:00
commit e1b8a026cb
79 changed files with 2712 additions and 43 deletions

View file

@ -16,6 +16,7 @@ dependencies {
api(projects.features.markets.api)
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
api(projects.features.tokenRecieve.api)
/* Data */
implementation(projects.data.common)
@ -39,6 +40,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.notifications.models)
implementation(projects.domain.transaction)
// FIXME [REDACTED_TASK_KEY]
// Remove the "Buy" and "Sell" actions from the redux middleware.

View file

@ -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<TokenMarketInfo.Network>) = 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(

View file

@ -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<MyPortfolioUM> get() = _state
@ -80,6 +92,8 @@ internal class MarketsPortfolioModel @Inject constructor(
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = 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
}
}
}

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.nft.api)
implementation(projects.features.tokenRecieve.api)
/** Core modules */
implementation(projects.core.configToggles)
@ -34,6 +35,8 @@ dependencies {
implementation(projects.domain.nft.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction)
implementation(projects.domain.tokens)
/** Common */
implementation(projects.common.ui)

View file

@ -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<NFTRoute>()
@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
walletName = params.walletName,
onBackClick = ::onChildBack,
),
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
)
private fun getDetailsComponent(

View file

@ -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,

View file

@ -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<TokenReceiveConfig> = 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,
)
}
}

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.features.manageTokens.api)
implementation(projects.features.biometry.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.tokenRecieve.api)
/** Core modules */
implementation(projects.core.configToggles)
@ -49,6 +50,7 @@ dependencies {
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.onramp)
implementation(projects.domain.transaction)
/** Tangem libraries */
implementation(projects.libs.tangemSdkApi)

View file

@ -24,8 +24,8 @@ import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
import dagger.assisted.Assisted

View file

@ -2,8 +2,8 @@ package com.tangem.features.onboarding.v2.twin.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -42,7 +42,6 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
BackHandler { model.onBack() }
val state by model.uiState.collectAsStateWithLifecycle()
OnboardingTwin(

View file

@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
@ -23,6 +24,7 @@ internal sealed class SendAnalyticEvents(
val feeType: AnalyticsParam.FeeType,
val blockchain: String,
val nonceNotEmpty: Boolean,
private val ensStatus: AnalyticsParam.EnsStatus,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",
params = mapOf(
@ -30,6 +32,10 @@ internal sealed class SendAnalyticEvents(
FEE_TYPE to feeType.value,
BLOCKCHAIN to blockchain,
NONCE to nonceNotEmpty.toString().capitalize(),
ENS_ADDRESS to when (ensStatus) {
AnalyticsParam.EnsStatus.EMPTY -> false.toString()
AnalyticsParam.EnsStatus.FULL -> true.toString()
},
),
)

View file

@ -28,6 +28,7 @@ internal class SendAnalyticHelper @Inject constructor(
feeType = feeType,
blockchain = cryptoCurrency.network.name,
nonceNotEmpty = feeSelectorUM.nonce != null,
ensStatus = getEnsStatus(sendUM),
),
)
analyticsEventHandler.send(
@ -52,4 +53,14 @@ internal class SendAnalyticHelper @Inject constructor(
else -> Basic.TransactionSent.MemoType.Null
}
}
private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus {
val blockchainAddressForEns =
(sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress
return if (blockchainAddressForEns != null) {
AnalyticsParam.EnsStatus.FULL
} else {
AnalyticsParam.EnsStatus.EMPTY
}
}
}

1
features/token-recieve/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.tokenrecieve.api"
}
dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/** Common */
implementation(projects.common.ui)
/** Domain */
implementation(projects.domain.models)
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.tokenreceive
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.TokenReceiveConfig
interface TokenReceiveComponent : ComposableBottomSheetComponent {
data class Params(
val config: TokenReceiveConfig,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, TokenReceiveComponent> {
override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.tokenreceive
interface TokenReceiveFeatureToggle {
val isNewTokenReceiveEnabled: Boolean
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,58 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.tokenreceive.impl"
}
dependencies {
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
implementation(deps.lifecycle.compose)
implementation(deps.kotlin.serialization)
implementation(deps.decompose.ext.compose)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Common */
implementation(projects.common.ui)
/** Core modules */
implementation(projects.common.routing)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.decompose)
implementation(projects.core.res)
implementation(projects.core.configToggles)
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
/** Feature Apis */
implementation(projects.features.tokenRecieve.api)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.tokenreceive
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
internal class DefaultTokenReceiveFeatureToggle(
private val featureTogglesManager: FeatureTogglesManager,
) : TokenReceiveFeatureToggle {
override val isNewTokenReceiveEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("NEW_TOKEN_RECEIVE_ENABLED")
}

View file

@ -0,0 +1,131 @@
package com.tangem.features.tokenreceive.component
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.ComposableContentComponent
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.TokenReceiveContentSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultTokenReceiveComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: TokenReceiveComponent.Params,
) : TokenReceiveComponent, AppComponentContext by appComponentContext {
private val model: TokenReceiveModel = getOrCreateModel(params)
private val innerRouter = InnerRouter<TokenReceiveRoutes>(
stackNavigation = model.stackNavigation,
popCallback = { onChildBack() },
)
private val contentStack = childStack(
key = "TokenReceiveFlowStack",
source = model.stackNavigation,
serializer = TokenReceiveRoutes.serializer(),
initialConfiguration = getInitialConfig(model.params.config.shouldShowWarning),
handleBackButton = false,
childFactory = ::screenChild,
)
@Composable
override fun BottomSheet() {
val content by contentStack.subscribeAsState()
val currentRoute = content.active.configuration
TokenReceiveContentSheet(
route = currentRoute,
onCloseClick = ::dismiss,
onBackClick = ::onChildBack,
contentStack = content,
)
}
override fun dismiss() {
model.params.onDismiss()
}
private fun onChildBack() {
when (contentStack.value.active.configuration) {
is TokenReceiveRoutes.QrCode -> model.stackNavigation.pop()
TokenReceiveRoutes.ReceiveAssets,
TokenReceiveRoutes.Warning,
-> dismiss()
}
}
private fun screenChild(
config: TokenReceiveRoutes,
componentContext: ComponentContext,
): ComposableContentComponent {
val appComponentContext = childByContext(
componentContext = componentContext,
router = innerRouter,
)
return when (config) {
is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent(
appComponentContext = appComponentContext,
params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams(
network = model.params.config.cryptoCurrency.network.name,
address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"),
callback = model,
onDismiss = ::dismiss,
id = config.addressId,
),
)
TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent(
appComponentContext = appComponentContext,
params = TokenReceiveAssetsComponent.TokenReceiveAssetsParams(
addresses = model.state.value.addresses,
callback = model,
onDismiss = ::dismiss,
notificationConfigs = model.state.value.notificationConfigs,
showMemoDisclaimer = model.params.config.showMemoDisclaimer,
fullName = model.params.config.cryptoCurrency.network.name,
tokenName = model.getTokenName(),
),
)
TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent(
appComponentContext = appComponentContext,
params = TokenReceiveWarningComponent.TokenReceiveWarningParams(
iconState = model.state.value.iconState,
callback = model,
onDismiss = ::dismiss,
network = model.params.config.cryptoCurrency.network,
),
)
}
}
private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes {
return if (shouldShowWarning) {
TokenReceiveRoutes.Warning
} else {
TokenReceiveRoutes.ReceiveAssets
}
}
@AssistedFactory
interface Factory : TokenReceiveComponent.Factory {
override fun create(
context: AppComponentContext,
params: TokenReceiveComponent.Params,
): DefaultTokenReceiveComponent
}
}
internal interface TokenReceiveModelCallback :
TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback,
TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback,
TokenReceiveWarningComponent.TokenReceiveWarningModelCallback

View file

@ -0,0 +1,44 @@
package com.tangem.features.tokenreceive.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal class TokenReceiveAssetsComponent(
appComponentContext: AppComponentContext,
private val params: TokenReceiveAssetsParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenReceiveAssetsContent(assetsUM = state)
}
internal interface TokenReceiveAssetsModelCallback {
fun onQrCodeClick(id: Int)
fun onCopyClick(id: Int)
}
data class TokenReceiveAssetsParams(
val notificationConfigs: ImmutableList<NotificationUM>,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val callback: TokenReceiveAssetsModelCallback,
val onDismiss: () -> Unit,
val showMemoDisclaimer: Boolean,
val fullName: String,
val tokenName: String,
)
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.tokenreceive.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel
import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent
internal class TokenReceiveQrCodeComponent(
appComponentContext: AppComponentContext,
private val params: TokenReceiveQrCodeParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TokenReceiveQrCodeModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenReceiveQrCodeContent(qrCodeUM = state)
}
internal interface TokenReceiveQrCodeModelCallback {
fun onCopyClick(id: Int)
fun onShareClick(address: String)
}
data class TokenReceiveQrCodeParams(
val id: Int,
val network: String,
val address: ReceiveAddress,
val callback: TokenReceiveQrCodeModelCallback,
val onDismiss: () -> Unit,
)
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.tokenreceive.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.network.Network
import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel
import com.tangem.features.tokenreceive.ui.TokenReceiveWarningContent
internal class TokenReceiveWarningComponent(
appComponentContext: AppComponentContext,
private val params: TokenReceiveWarningParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TokenReceiveWarningModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenReceiveWarningContent(warningUM = state)
}
internal interface TokenReceiveWarningModelCallback {
fun onWarningAcknowledged()
}
data class TokenReceiveWarningParams(
val iconState: CurrencyIconState,
val callback: TokenReceiveWarningModelCallback,
val onDismiss: () -> Unit,
val network: Network,
)
}

View file

@ -0,0 +1,68 @@
package com.tangem.features.tokenreceive.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tokenreceive.DefaultTokenReceiveFeatureToggle
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.tokenreceive.component.DefaultTokenReceiveComponent
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
import com.tangem.features.tokenreceive.model.TokenReceiveModel
import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel
import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureToggleModule {
@Provides
@Singleton
fun provideTokenReceiveFeatureToggle(featureTogglesManager: FeatureTogglesManager): TokenReceiveFeatureToggle {
return DefaultTokenReceiveFeatureToggle(
featureTogglesManager = featureTogglesManager,
)
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindComponent(factory: DefaultTokenReceiveComponent.Factory): TokenReceiveComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds
@IntoMap
@ClassKey(TokenReceiveModel::class)
fun bindsTokenReceiveModel(model: TokenReceiveModel): Model
@Binds
@IntoMap
@ClassKey(TokenReceiveAssetsModel::class)
fun bindsTokenReceiveAssetsModel(model: TokenReceiveAssetsModel): Model
@Binds
@IntoMap
@ClassKey(TokenReceiveQrCodeModel::class)
fun bindsTokenReceiveQrCodeModel(model: TokenReceiveQrCodeModel): Model
@Binds
@IntoMap
@ClassKey(TokenReceiveWarningModel::class)
fun bindsTokenReceiveWarningModel(model: TokenReceiveWarningModel): Model
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.tokenreceive.entity
import com.tangem.core.ui.extensions.TextReference
internal data class ReceiveAddress(
val value: String,
val type: Type,
) {
sealed interface Type {
data object Ens : Type
data class Default(
val displayName: TextReference,
) : Type
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.tokenreceive.model
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TokenReceiveAssetsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<TokenReceiveAssetsComponent.TokenReceiveAssetsParams>()
init {
analyticsEventHandler.send(
TokenReceiveNewAnalyticsEvent.ReceiveScreenOpened(
token = params.tokenName,
blockchainName = params.fullName,
ensStatus = configureEnsStatus(),
),
)
}
internal val state: StateFlow<ReceiveAssetsUM>
field = MutableStateFlow<ReceiveAssetsUM>(
ReceiveAssetsUM(
onCopyClick = params.callback::onCopyClick,
onOpenQrCodeClick = params.callback::onQrCodeClick,
addresses = params.addresses,
showMemoDisclaimer = params.showMemoDisclaimer,
isEnsResultLoading = false,
notificationConfigs = params.notificationConfigs,
fullName = params.fullName,
),
)
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens }
return if (hasEnsAddress) {
AnalyticsParam.EnsStatus.FULL
} else {
AnalyticsParam.EnsStatus.EMPTY
}
}
}

View file

@ -0,0 +1,219 @@
package com.tangem.features.tokenreceive.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.push
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.R
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.Asset
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class TokenReceiveModel @Inject constructor(
private val clipboardManager: ClipboardManager,
private val shareManager: ShareManager,
private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), TokenReceiveModelCallback {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
val params = paramsContainer.require<TokenReceiveComponent.Params>()
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
internal val state: StateFlow<TokenReceiveUM>
field = MutableStateFlow<TokenReceiveUM>(getInitState())
init {
modelScope.launch {
load()
}
}
override fun onQrCodeClick(id: Int) {
analyticsEventHandler.send(
TokenReceiveNewAnalyticsEvent.QrScreenOpened(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
),
)
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
}
override fun onCopyClick(id: Int) {
val addressToCopy = state.value.addresses[id] ?: return
sendCopyActionAnalytic(addressToCopy)
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
}
override fun onShareClick(address: String) {
shareManager.shareText(text = address)
}
override fun onWarningAcknowledged() {
modelScope.launch {
saveViewedTokenReceiveWarningUseCase.invoke(
when (val asset = params.config.asset) {
Asset.Currency -> params.config.cryptoCurrency.name
Asset.NFT -> asset.name
},
)
stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets)
}
}
internal fun getTokenName(): String {
return when (val asset = params.config.asset) {
Asset.Currency -> params.config.cryptoCurrency.symbol
Asset.NFT -> asset.name
}
}
private fun mapAddresses(addresses: List<ReceiveAddressModel>): ImmutableMap<Int, ReceiveAddress> {
return buildMap {
addresses.mapIndexed { index, model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
ReceiveAddress.Type.Default(
displayName = stringReference(model.displayName),
)
}
ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens
}
put(
key = index,
value = ReceiveAddress(
value = model.value,
type = type,
),
)
}
}.toPersistentMap()
}
private suspend fun load() = withContext(dispatchers.default) {
state.value = state.value.copy(
isEnsResultLoading = true,
)
val reverseResolveResult =
if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) {
getReverseResolvedEnsAddressUseCase(
userWalletId = params.config.userWalletId,
network = params.config.cryptoCurrency.network,
addresses = params.config.receiveAddress.map { it.value },
)
} else {
emptyList()
}
val currentAddressValues = state.value.addresses.values.map { it.value }.toSet()
val newEnsAddresses = reverseResolveResult
.filterIsInstance<EnsAddress.Address>()
.filterNot { it.name in currentAddressValues }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) }
val combinedAddresses = (state.value.addresses.values + newEnsAddresses)
.sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens })
val updatedAddresses = combinedAddresses
.mapIndexed { index, address -> index to address }
.toMap()
.toPersistentMap()
state.value = state.value.copy(
isEnsResultLoading = false,
addresses = updatedAddresses,
)
}
private fun getNotifications(): List<NotificationUM> {
return buildList {
add(
NotificationUM.Info(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(
getTokenName(),
params.config.cryptoCurrency.network.name,
),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconTint = NotificationConfig.IconTint.Accent,
),
)
params.config.tokenReceiveNotification.map { notification ->
add(
NotificationUM.Warning(
title = resourceReference(notification.title),
subtitle = resourceReference(notification.subtitle),
),
)
}
}
}
private fun getInitState(): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(params.config.receiveAddress),
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
network = params.config.cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications().toImmutableList(),
)
}
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) {
val event = when (receiveAddress.type) {
is ReceiveAddress.Type.Default -> {
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
)
}
ReceiveAddress.Type.Ens -> {
TokenReceiveNewAnalyticsEvent.ButtonCopyEns(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
)
}
}
analyticsEventHandler.send(event)
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.tokenreceive.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TokenReceiveQrCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams>()
internal val state: StateFlow<QrCodeUM>
field = MutableStateFlow<QrCodeUM>(
QrCodeUM(
network = params.network,
addressValue = params.address.value,
addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY,
onCopyClick = { params.callback.onCopyClick(params.id) },
onShareClick = params.callback::onShareClick,
),
)
}

View file

@ -0,0 +1,33 @@
package com.tangem.features.tokenreceive.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.iconResId
import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent
import com.tangem.features.tokenreceive.ui.state.WarningUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TokenReceiveWarningModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<TokenReceiveWarningComponent.TokenReceiveWarningParams>()
internal val state: StateFlow<WarningUM>
field = MutableStateFlow<WarningUM>(
WarningUM(
iconState = params.iconState,
onWarningAcknowledged = params.callback::onWarningAcknowledged,
network = params.network.name,
networkIcon = params.network.iconResId,
),
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.tokenreceive.route
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Immutable
@Serializable
internal sealed interface TokenReceiveRoutes : Route {
@Serializable
data object Warning : TokenReceiveRoutes
@Serializable
data object ReceiveAssets : TokenReceiveRoutes
@Serializable
data class QrCode(val addressId: Int) : TokenReceiveRoutes
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.tokenreceive.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
@Composable
internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
Box {
content()
CopiedTextSnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
)
}
}

View file

@ -0,0 +1,346 @@
package com.tangem.features.tokenreceive.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.launch
@Composable
internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) {
val snackbarHostState = remember(::SnackbarHostState)
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.tertiary)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 8.dp,
),
) {
SpacerH8()
Info(
showMemoDisclaimer = assetsUM.showMemoDisclaimer,
notificationConfigs = assetsUM.notificationConfigs,
)
SpacerH12()
AddressBlock(
onCopyClick = assetsUM.onCopyClick,
onOpenQrCodeClick = assetsUM.onOpenQrCodeClick,
addresses = assetsUM.addresses,
snackbarHostState = snackbarHostState,
fullName = assetsUM.fullName,
)
if (assetsUM.isEnsResultLoading) {
LoadingBlock()
}
}
}
}
@Composable
private fun Info(
notificationConfigs: ImmutableList<NotificationUM>,
showMemoDisclaimer: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
if (showMemoDisclaimer) {
Text(
modifier = Modifier
.padding(horizontal = 18.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
notificationConfigs.fastForEach {
key(it.hashCode()) {
Notification(config = it.config)
}
}
}
}
@Composable
private fun AddressBlock(
onOpenQrCodeClick: (id: Int) -> Unit,
onCopyClick: (id: Int) -> Unit,
addresses: ImmutableMap<Int, ReceiveAddress>,
snackbarHostState: SnackbarHostState,
fullName: String,
) {
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
addresses.entries.toList().fastForEach { entry ->
when (entry.value.type) {
is ReceiveAddress.Type.Default -> {
key(entry.key) {
AddressItem(
onCopyClick = {
onCopyClick(entry.key)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(
R.string.wallet_notification_address_copied,
),
)
}
},
onOpenQrCodeClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onOpenQrCodeClick(entry.key)
},
fullName = fullName,
address = entry.value.value,
)
SpacerH8()
}
}
ReceiveAddress.Type.Ens -> {
key(entry.key) {
EnsItem(
onCopyClick = {
onCopyClick(entry.key)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(
R.string.wallet_notification_address_copied,
),
)
}
},
address = entry.value.value,
)
SpacerH8()
}
}
}
}
}
@Composable
private fun AddressItem(
onOpenQrCodeClick: () -> Unit,
fullName: String,
onCopyClick: () -> Unit,
address: String,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
onClick = onOpenQrCodeClick,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 14.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
IdentIcon(
address = address,
modifier = Modifier
.size(size = 36.dp)
.clip(shape = RoundedCornerShape(18.dp)),
)
SpacerW12()
Column(modifier = Modifier.weight(1f)) {
EllipsisText(
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName),
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
)
EllipsisText(
text = address,
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
)
}
SpacerW12()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_qrcode_new_24,
onClick = onOpenQrCodeClick,
)
SpacerW8()
TangemIconButton(
modifier = Modifier.size(TangemTheme.dimens.size28),
iconRes = R.drawable.ic_copy_new_24,
onClick = onCopyClick,
)
}
}
}
@Composable
private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 14.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.size(36.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_ens_36),
contentDescription = null,
)
SpacerW12()
EllipsisText(
modifier = Modifier.weight(1f),
text = address,
ellipsis = TextEllipsis.Middle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
)
TangemIconButton(
modifier = Modifier.size(28.dp),
iconRes = R.drawable.ic_copy_new_24,
onClick = onCopyClick,
)
}
}
}
@Composable
private fun LoadingBlock(modifier: Modifier = Modifier) {
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
) {
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = 64.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
modifier = Modifier.padding(TangemTheme.dimens.spacing8),
)
}
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TokenReceiveAssetsContent(
@PreviewParameter(TokenReceiveAssetsContentProvider::class) params: ReceiveAssetsUM,
) {
TangemThemePreview {
TokenReceiveAssetsContent(assetsUM = params)
}
}
private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<ReceiveAssetsUM> {
val address = ReceiveAddress(
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
type = ReceiveAddress.Type.Default(
displayName = stringReference("Etherium address"),
),
)
private val config = ReceiveAssetsUM(
notificationConfigs =
persistentListOf(
NotificationUM.Warning(
title = stringReference("Send only XLM on the Ethereum network"),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
),
),
addresses = persistentMapOf(
0 to address,
1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
),
showMemoDisclaimer = false,
onCopyClick = {},
onOpenQrCodeClick = {},
isEnsResultLoading = true,
fullName = "Etherium",
)
override val values: Sequence<ReceiveAssetsUM>
get() = sequenceOf(config)
}

View file

@ -0,0 +1,93 @@
package com.tangem.features.tokenreceive.ui
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.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<TokenReceiveRoutes, ComposableContentComponent>,
) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
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<TokenReceiveRoutes, ComposableContentComponent>,
modifier: Modifier = Modifier,
) {
Children(
stack = stackState,
animation = stackAnimation(fade()),
modifier = modifier
.fillMaxSize()
.animateContentSize(),
) {
it.instance.Content(Modifier.fillMaxSize())
}
}

View file

@ -0,0 +1,195 @@
package com.tangem.features.tokenreceive.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokenreceive.impl.R
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
import kotlinx.coroutines.launch
@Composable
internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) {
val snackbarHostState = remember(::SnackbarHostState)
val qrCodePainter = rememberQrPainter(content = qrCodeUM.addressValue)
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.tertiary)
.padding(bottom = 16.dp)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH8()
QrCodePage(
addressFullName = qrCodeUM.addressName,
addressValue = qrCodeUM.addressValue,
network = qrCodeUM.network,
qrCodePainter = qrCodePainter,
)
SpacerH24()
Buttons(
onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) },
onCopyClick = qrCodeUM.onCopyClick,
snackbarHostState = snackbarHostState,
)
}
}
}
@Composable
private fun QrCodePage(addressFullName: TextReference, addressValue: String, network: String, qrCodePainter: Painter) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) {
Text(
text = stringResourceSafe(
R.string.receive_bottom_sheet_warning_message_compact,
addressFullName.resolveReference(),
network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
SpacerH(20.dp)
Image(
painter = qrCodePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.size(248.dp),
)
SpacerH24()
}
Text(
text = stringResourceSafe(R.string.wc_common_address),
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle2,
)
SpacerH2()
Text(
text = addressValue,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
@Composable
private fun Buttons(
snackbarHostState: SnackbarHostState,
onShareClick: () -> Unit,
onCopyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onCopyClick()
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onShareClick()
},
)
}
}
@Composable
private fun rememberQrPainter(content: String, size: Dp = 248.dp, padding: Dp = 0.dp): BitmapPainter {
val density = LocalDensity.current
return remember(content) {
BitmapPainter(
content.toQrCode(
sizePx = with(density) { size.roundToPx() },
paddingPx = with(density) { padding.roundToPx() },
).asImageBitmap(),
)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TokenReceiveQrCodeContent(
@PreviewParameter(TokenReceiveQrCodeContentPreviewProvider::class) qrCodeUM: QrCodeUM,
) {
TangemThemePreview {
TokenReceiveQrCodeContent(qrCodeUM = qrCodeUM)
}
}
private class TokenReceiveQrCodeContentPreviewProvider : PreviewParameterProvider<QrCodeUM> {
private val config = QrCodeUM(
network = "Ethereum",
addressName = stringReference("Etherium"),
addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
onCopyClick = {},
onShareClick = {},
)
override val values: Sequence<QrCodeUM>
get() = sequenceOf(config)
}

View file

@ -0,0 +1,142 @@
package com.tangem.features.tokenreceive.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokenreceive.impl.R
import com.tangem.features.tokenreceive.ui.state.WarningUM
@Composable
internal fun TokenReceiveWarningContent(warningUM: WarningUM) {
val hapticFeedback = LocalHapticFeedback.current
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.tertiary)
.padding(
start = 16.dp,
end = 16.dp,
bottom = 16.dp,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CurrencyIcon(
modifier = Modifier.size(size = 56.dp),
state = warningUM.iconState,
shouldDisplayNetwork = false,
iconSize = 56.dp,
)
SpacerH24()
WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network)
SpacerH12()
Text(
textAlign = TextAlign.Center,
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_description),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
SpacerH(48.dp)
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_got_it),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
warningUM.onWarningAcknowledged()
},
)
}
}
@Composable
fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
textAlign = TextAlign.Center,
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
modifier = Modifier.size(20.dp),
painter = painterResource(id = networkIcon),
tint = Color.Unspecified,
contentDescription = null,
)
Text(
textAlign = TextAlign.Center,
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TokenReceiveWarningContent(
@PreviewParameter(TokenReceiveWarningContentProvider::class) warningUM: WarningUM,
) {
TangemThemePreview {
TokenReceiveWarningContent(warningUM = warningUM)
}
}
private class TokenReceiveWarningContentProvider : PreviewParameterProvider<WarningUM> {
val iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = null,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
showCustomBadge = false,
)
override val values: Sequence<WarningUM>
get() = sequenceOf(
WarningUM(
iconState = iconState,
onWarningAcknowledged = {},
network = "Etherium",
networkIcon = R.drawable.ic_eth_16,
),
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.tokenreceive.ui.state
import com.tangem.core.ui.extensions.TextReference
internal data class QrCodeUM(
val onCopyClick: () -> Unit,
val onShareClick: (String) -> Unit,
val addressName: TextReference,
val addressValue: String,
val network: String,
)

View file

@ -0,0 +1,16 @@
package com.tangem.features.tokenreceive.ui.state
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal data class ReceiveAssetsUM(
val showMemoDisclaimer: Boolean,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val onOpenQrCodeClick: (id: Int) -> Unit,
val onCopyClick: (id: Int) -> Unit,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
val fullName: String,
)

View file

@ -0,0 +1,15 @@
package com.tangem.features.tokenreceive.ui.state
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal data class TokenReceiveUM(
val network: String,
val iconState: CurrencyIconState,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
)

View file

@ -0,0 +1,10 @@
package com.tangem.features.tokenreceive.ui.state
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
internal data class WarningUM(
val network: String,
val networkIcon: Int,
val iconState: CurrencyIconState,
val onWarningAcknowledged: () -> Unit,
)

View file

@ -101,5 +101,8 @@ dependencies {
implementation(projects.features.swap.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.tokenRecieve.api)
implementation(deps.decompose.ext.compose)
}

View file

@ -4,16 +4,24 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -25,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
@Assisted params: TokenDetailsComponent.Params,
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
txHistoryComponentFactory: TxHistoryComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
private val model: TokenDetailsModel = getOrCreateModel(params)
@ -37,6 +46,13 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
),
)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = TokenReceiveConfig.serializer(),
handleBackButton = false,
childFactory = ::bottomSheetChild,
)
init {
lifecycle.subscribe(
onPause = model::onPause,
@ -54,12 +70,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
NavigationBar3ButtonsScrim()
TokenDetailsScreen(
state = state,
tokenMarketBlockComponent = tokenMarketBlockComponent,
txHistoryComponent = txHistoryComponent,
)
bottomSheet.child?.instance?.BottomSheet()
}
private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? {
@ -68,6 +86,17 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
return TokenMarketBlockComponent.Params(cryptoCurrency = this)
}
private fun bottomSheetChild(
config: TokenReceiveConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
context = childByContext(componentContext),
params = TokenReceiveComponent.Params(
config = config,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
@AssistedFactory
interface Factory : TokenDetailsComponent.Factory {
override fun create(

View file

@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable
import androidx.paging.cachedIn
import arrow.core.getOrElse
import arrow.core.merge
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
@ -35,8 +37,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -60,10 +65,7 @@ import com.tangem.domain.transaction.error.AssociateAssetError
import com.tangem.domain.transaction.error.IncompleteTransactionError
import com.tangem.domain.transaction.error.OpenTrustlineError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.AssociateAssetUseCase
import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase
import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase
import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
@ -82,6 +84,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
@ -137,6 +140,9 @@ internal class TokenDetailsModel @Inject constructor(
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
) : Model(), TokenDetailsClickIntents {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
@ -159,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor(
/** Transaction id to check for status */
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
@ -594,20 +602,27 @@ internal class TokenDetailsModel @Inject constructor(
}
modelScope.launch {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
networkAddress = networkAddress,
onCopyClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol))
shareManager.shareText(text = it)
},
)
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
bottomSheetNavigation.activate(
configuration = configureReceiveAddresses(addresses = networkAddress),
)
} else {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
currency = cryptoCurrency,
networkAddress = networkAddress,
onCopyClick = {
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
analyticsEventsHandler.send(
TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol),
)
shareManager.shareText(text = it)
},
)
}
}
}
@ -1061,6 +1076,43 @@ internal class TokenDetailsModel @Inject constructor(
.launchIn(modelScope)
}
private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig {
val ensName = getEnsNameUseCase.invoke(
userWalletId = userWalletId,
network = cryptoCurrency.network,
address = addresses.defaultAddress.value,
)
val receiveAddresses = buildList {
ensName?.let { ens ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Ens,
value = ens,
displayName = ens,
),
)
}
addresses.availableAddresses.map { address ->
add(
ReceiveAddressModel(
nameService = ReceiveAddressModel.NameService.Default,
value = address.value,
displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})",
),
)
}
}
return TokenReceiveConfig(
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
cryptoCurrency = cryptoCurrency,
userWalletId = userWalletId,
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
receiveAddress = receiveAddresses,
)
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -96,6 +96,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications)
implementation(projects.domain.notifications.toggles)
implementation(projects.domain.transaction)
/** Feature Apis */
implementation(projects.features.details.api)
@ -114,6 +115,7 @@ dependencies {
implementation(projects.features.nft.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.kyc.api)
implementation(projects.features.tokenRecieve.api)
/** Common modules */
implementation(projects.common)

View file

@ -24,11 +24,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,
@ -36,6 +38,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()
@ -78,6 +81,15 @@ internal class WalletComponent @AssistedInject constructor(
source = AppRoute.PushNotification.Source.Main,
),
)
is WalletDialogConfig.TokenReceive -> {
tokenReceiveComponentFactory.create(
context = childByContext(componentContext),
params = TokenReceiveComponent.Params(
config = dialogConfig.tokenReceiveConfig,
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
),
)
}
}
},
)

View file

@ -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,
)
}
}

View file

@ -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),
)
}
}

View file

@ -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)
}

View file

@ -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 = {}),

View file

@ -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
}