Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 11:01:21 +02:00
parent f0de027151
commit 33fd5a927a
21 changed files with 825 additions and 20 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

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

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

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

@ -51,6 +51,7 @@ dependencies {
implementation(projects.domain.transaction)
implementation(projects.domain.transaction.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
/** Feature Apis */
implementation(projects.features.tokenRecieve.api)

View file

@ -0,0 +1,197 @@
package com.tangem.features.tokenreceive.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.tokenreceive.model.TokenReceiveModel
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.TokenReceiveContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultTokenReceiveComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: TokenReceiveComponent.Params,
) : TokenReceiveComponent, AppComponentContext by appComponentContext {
private val model: TokenReceiveModel = getOrCreateModel(params)
private val innerRouter = InnerRouter<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,
),
)
TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent(
appComponentContext = appComponentContext,
params = TokenReceiveWarningComponent.TokenReceiveWarningParams(
iconState = model.state.value.iconState,
callback = model,
onDismiss = ::dismiss,
network = model.params.config.cryptoCurrency.network,
),
)
}
}
private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes {
return if (shouldShowWarning) {
TokenReceiveRoutes.Warning
} else {
TokenReceiveRoutes.ReceiveAssets
}
}
@AssistedFactory
interface Factory : TokenReceiveComponent.Factory {
override fun create(
context: AppComponentContext,
params: TokenReceiveComponent.Params,
): DefaultTokenReceiveComponent
}
}
@Composable
internal fun TokenReceiveContentSheet(
route: TokenReceiveRoutes,
onCloseClick: () -> Unit,
onBackClick: () -> Unit,
contentStack: ChildStack<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,
)
}
}
}
internal interface TokenReceiveModelCallback :
TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback,
TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback,
TokenReceiveWarningComponent.TokenReceiveWarningModelCallback

View file

@ -0,0 +1,43 @@
package com.tangem.features.tokenreceive.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
internal class TokenReceiveAssetsComponent(
appComponentContext: AppComponentContext,
private val params: TokenReceiveAssetsParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenReceiveAssetsContent(assetsUM = state)
}
internal interface TokenReceiveAssetsModelCallback {
fun onQrCodeClick(id: Int)
fun onCopyClick(id: Int)
}
data class TokenReceiveAssetsParams(
val notificationConfigs: ImmutableList<NotificationUM>,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val callback: TokenReceiveAssetsModelCallback,
val onDismiss: () -> Unit,
val showMemoDisclaimer: Boolean,
val fullName: 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,35 @@
package com.tangem.features.tokenreceive.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TokenReceiveAssetsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<TokenReceiveAssetsComponent.TokenReceiveAssetsParams>()
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,
),
)
}

View file

@ -0,0 +1,186 @@
package com.tangem.features.tokenreceive.model
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.push
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.ui.R
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.Asset
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.domain.models.network.Network
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@ModelScoped
internal class TokenReceiveModel @Inject constructor(
private val clipboardManager: ClipboardManager,
private val shareManager: ShareManager,
private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase,
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model(), TokenReceiveModelCallback {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
val params = paramsContainer.require<TokenReceiveComponent.Params>()
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
internal val state: StateFlow<TokenReceiveUM>
field = MutableStateFlow<TokenReceiveUM>(getInitState())
init {
modelScope.launch {
load()
}
}
override fun onQrCodeClick(id: Int) {
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
}
override fun onCopyClick(id: Int) {
val addressToCopy = state.value.addresses[id] ?: return
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
}
override fun onShareClick(address: String) {
shareManager.shareText(text = address)
}
override fun onWarningAcknowledged() {
modelScope.launch {
saveViewedTokenReceiveWarningUseCase.invoke(
when (val asset = params.config.asset) {
Asset.Currency -> params.config.cryptoCurrency.name
Asset.NFT -> asset.name
},
)
stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets)
}
}
private fun mapAddresses(addresses: List<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(
when (val asset = params.config.asset) {
Asset.Currency -> params.config.cryptoCurrency.symbol
Asset.NFT -> asset.name
},
params.config.cryptoCurrency.network.name,
),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconTint = NotificationConfig.IconTint.Accent,
),
)
params.config.tokenReceiveNotification.map { notification ->
add(
NotificationUM.Warning(
title = resourceReference(notification.title),
subtitle = resourceReference(notification.subtitle),
),
)
}
}
}
private fun getInitState(): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(params.config.receiveAddress),
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
network = params.config.cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications().toImmutableList(),
)
}
}

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.stringReference
import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class TokenReceiveQrCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<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 ?: stringReference(""),
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

@ -60,7 +60,7 @@ internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) {
Buttons(
onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) },
onCopyClick = { qrCodeUM.onCopyClick(qrCodeUM.addressValue) },
onCopyClick = qrCodeUM.onCopyClick,
snackbarHostState = snackbarHostState,
)
}

View file

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

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)
@ -113,6 +114,7 @@ dependencies {
implementation(projects.features.biometry.api)
implementation(projects.features.nft.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.tokenRecieve.api)
/** Common modules */
implementation(projects.common)