Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-03 11:31:02 +02:00
parent f80ce6c175
commit 74a746b237
11 changed files with 204 additions and 167 deletions

View file

@ -78,10 +78,11 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor(
appComponentContext = appComponentContext,
params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams(
cryptoCurrency = model.params.config.cryptoCurrency,
address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"),
address = model.state.value.addresses.find { it.value == config.address } ?: error(
"Address has to be there",
),
callback = model,
onDismiss = ::dismiss,
id = config.addressId,
),
)
TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent(

View file

@ -13,7 +13,6 @@ 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,
@ -29,13 +28,13 @@ internal class TokenReceiveAssetsComponent(
}
internal interface TokenReceiveAssetsModelCallback {
fun onQrCodeClick(id: Int)
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
fun onQrCodeClick(address: String)
fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource)
}
data class TokenReceiveAssetsParams(
val notificationConfigs: ImmutableList<NotificationUM>,
val addresses: ImmutableMap<Int, ReceiveAddress>,
val addresses: ImmutableList<ReceiveAddress>,
val callback: TokenReceiveAssetsModelCallback,
val onDismiss: () -> Unit,
val showMemoDisclaimer: Boolean,

View file

@ -27,12 +27,11 @@ internal class TokenReceiveQrCodeComponent(
}
internal interface TokenReceiveQrCodeModelCallback {
fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource)
fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource)
fun onShareClick(address: String)
}
data class TokenReceiveQrCodeParams(
val id: Int,
val cryptoCurrency: CryptoCurrency,
val address: ReceiveAddress,
val callback: TokenReceiveQrCodeModelCallback,

View file

@ -0,0 +1,156 @@
package com.tangem.features.tokenreceive.entity
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.TokenReceiveNotification
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.ens.EnsAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Primary
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.Provider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
internal class TokenReceiveStateFactory(
private val currentStateProvider: Provider<TokenReceiveUM>,
private val cryptoCurrency: CryptoCurrency,
private val addresses: List<ReceiveAddressModel>,
private val tokenReceiveNotification: List<TokenReceiveNotification>,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
fun getInitialState(tokenName: String): TokenReceiveUM {
return TokenReceiveUM(
addresses = mapAddresses(
addresses = addresses,
cryptoCurrency = cryptoCurrency,
),
iconState = iconStateConverter.convert(cryptoCurrency),
network = cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications(
tokenName = tokenName,
tokenReceiveNotification = tokenReceiveNotification,
).toImmutableList(),
)
}
fun getLoadingState(): TokenReceiveUM {
return currentStateProvider.invoke().copy(isEnsResultLoading = true)
}
fun getLoadedState(ensAddress: List<EnsAddress>): TokenReceiveUM {
val currentAddressValues = currentStateProvider.invoke().addresses.map { it.value }.toSet()
val newEnsAddresses = ensAddress
.filterIsInstance<EnsAddress.Address>()
.filterNot { it.name in currentAddressValues }
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = Ens) }
val combinedAddresses = (currentStateProvider.invoke().addresses + newEnsAddresses)
.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
return currentStateProvider.invoke().copy(
isEnsResultLoading = false,
addresses = combinedAddresses.toPersistentList(),
)
}
private fun mapAddresses(
addresses: List<ReceiveAddressModel>,
cryptoCurrency: CryptoCurrency,
): ImmutableList<ReceiveAddress> {
val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy }
val receiveAddresses = addresses.map { model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
val displayName = when (cryptoCurrency) {
is CryptoCurrency.Coin -> cryptoCurrency.name
is CryptoCurrency.Token -> cryptoCurrency.symbol
}
Primary.Default(
displayName = if (needUseToLegacyAndDefaultName) {
TextReference.Res(R.string.domain_receive_assets_default_address)
} else {
TextReference.Combined(
wrappedList(
TextReference.Str(displayName),
TextReference.Str(" "),
TextReference.Res(R.string.common_address),
),
)
},
)
}
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
)
}
ReceiveAddress(
value = model.value,
type = type,
)
}
val sortedAddresses = receiveAddresses.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
return sortedAddresses.toPersistentList()
}
private fun getNotifications(
tokenName: String,
tokenReceiveNotification: List<TokenReceiveNotification>,
): List<NotificationUM> {
return buildList {
add(
NotificationUM.Info(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(
tokenName,
cryptoCurrency.network.name,
),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconTint = NotificationConfig.IconTint.Accent,
),
)
tokenReceiveNotification.map { notification ->
add(
NotificationUM.Warning(
title = resourceReference(notification.title),
subtitle = resourceReference(notification.subtitle),
),
)
}
}
}
}

View file

@ -41,7 +41,7 @@ internal class TokenReceiveAssetsModel @Inject constructor(
ReceiveAssetsUM(
onCopyClick = {
params.callback.onCopyClick(
id = it,
address = it,
source = TokenReceiveCopyActionSource.Receive,
)
},
@ -54,7 +54,7 @@ internal class TokenReceiveAssetsModel @Inject constructor(
)
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens }
val hasEnsAddress = params.addresses.any { it.type == ReceiveAddress.Type.Ens }
return if (hasEnsAddress) {
AnalyticsParam.EnsStatus.FULL
} else {

View file

@ -2,22 +2,13 @@ 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.TextReference
import com.tangem.core.ui.extensions.resourceReference
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.TokenReceiveCopyActionSource
@ -28,12 +19,11 @@ import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
import com.tangem.features.tokenreceive.entity.ReceiveAddress
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Ens
import com.tangem.features.tokenreceive.entity.ReceiveAddress.Type.Primary
import com.tangem.features.tokenreceive.entity.TokenReceiveStateFactory
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
import com.tangem.utils.Provider
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
@ -52,14 +42,21 @@ internal class TokenReceiveModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), TokenReceiveModelCallback {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val tokenReceiveStateFactory: TokenReceiveStateFactory by lazy {
TokenReceiveStateFactory(
cryptoCurrency = params.config.cryptoCurrency,
addresses = params.config.receiveAddress,
tokenReceiveNotification = params.config.tokenReceiveNotification,
currentStateProvider = Provider { state.value },
)
}
val params = paramsContainer.require<TokenReceiveComponent.Params>()
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
internal val state: StateFlow<TokenReceiveUM>
field = MutableStateFlow<TokenReceiveUM>(getInitState())
field = MutableStateFlow<TokenReceiveUM>(tokenReceiveStateFactory.getInitialState(getTokenName()))
init {
modelScope.launch {
@ -67,20 +64,19 @@ internal class TokenReceiveModel @Inject constructor(
}
}
override fun onQrCodeClick(id: Int) {
override fun onQrCodeClick(address: String) {
analyticsEventHandler.send(
TokenReceiveNewAnalyticsEvent.QrScreenOpened(
token = getTokenName(),
blockchainName = params.config.cryptoCurrency.network.name,
),
)
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(address = address))
}
override fun onCopyClick(id: Int, source: TokenReceiveCopyActionSource) {
val addressToCopy = state.value.addresses[id] ?: return
sendCopyActionAnalytic(addressToCopy, source)
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
override fun onCopyClick(address: ReceiveAddress, source: TokenReceiveCopyActionSource) {
sendCopyActionAnalytic(address, source)
clipboardManager.setText(text = address.value, isSensitive = true)
}
override fun onShareClick(address: String) {
@ -106,51 +102,8 @@ internal class TokenReceiveModel @Inject constructor(
}
}
private fun mapAddresses(
addresses: List<ReceiveAddressModel>,
networkName: String,
): ImmutableMap<Int, ReceiveAddress> {
val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy }
return buildMap {
addresses.mapIndexed { index, model ->
val type = when (model.nameService) {
ReceiveAddressModel.NameService.Default -> {
Primary.Default(
displayName = if (needUseToLegacyAndDefaultName) {
TextReference.Res(R.string.domain_receive_assets_default_address)
} else {
TextReference.Combined(
wrappedList(
TextReference.Str(networkName),
TextReference.Str(" "),
TextReference.Res(R.string.common_address),
),
)
},
)
}
ReceiveAddressModel.NameService.Ens -> Ens
ReceiveAddressModel.NameService.Legacy -> Primary.Legacy(
displayName = TextReference.Res(R.string.domain_receive_assets_legacy_address),
)
}
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,
)
state.value = tokenReceiveStateFactory.getLoadingState()
val reverseResolveResult =
if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) {
getReverseResolvedEnsAddressUseCase(
@ -161,74 +114,7 @@ internal class TokenReceiveModel @Inject constructor(
} 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 = Ens) }
val combinedAddresses = (state.value.addresses.values + newEnsAddresses)
.sortedWith(
compareBy { address ->
when (address.type) {
is Ens -> 0
is Primary.Default -> 1
is Primary.Legacy -> 2
}
},
)
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(
addresses = params.config.receiveAddress,
networkName = params.config.cryptoCurrency.network.name,
),
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
network = params.config.cryptoCurrency.network.name,
isEnsResultLoading = false,
notificationConfigs = getNotifications().toImmutableList(),
)
state.value = tokenReceiveStateFactory.getLoadedState(reverseResolveResult)
}
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress, source: TokenReceiveCopyActionSource) {

View file

@ -30,7 +30,7 @@ internal class TokenReceiveQrCodeModel @Inject constructor(
addressName = TextReference.Str("${params.cryptoCurrency.name} (${params.cryptoCurrency.symbol})"),
onCopyClick = {
params.callback.onCopyClick(
id = params.id,
address = params.address,
source = TokenReceiveCopyActionSource.QR,
)
},

View file

@ -15,5 +15,5 @@ internal sealed interface TokenReceiveRoutes : Route {
data object ReceiveAssets : TokenReceiveRoutes
@Serializable
data class QrCode(val addressId: Int) : TokenReceiveRoutes
data class QrCode(val address: String) : TokenReceiveRoutes
}

View file

@ -45,9 +45,7 @@ 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
@ -121,9 +119,9 @@ private fun Info(
@Composable
private fun AddressBlock(
onOpenQrCodeClick: (id: Int) -> Unit,
onCopyClick: (id: Int) -> Unit,
addresses: ImmutableMap<Int, ReceiveAddress>,
onOpenQrCodeClick: (address: String) -> Unit,
onCopyClick: (address: ReceiveAddress) -> Unit,
addresses: ImmutableList<ReceiveAddress>,
snackbarHostState: SnackbarHostState,
) {
val hapticFeedback = LocalHapticFeedback.current
@ -131,13 +129,13 @@ private fun AddressBlock(
val context = LocalContext.current
val resources = context.resources
addresses.entries.toList().fastForEach { entry ->
when (val type = entry.value.type) {
addresses.fastForEach { address ->
when (val type = address.type) {
is ReceiveAddress.Type.Primary -> {
key(entry.key) {
key(address.value) {
AddressItem(
onCopyClick = {
onCopyClick(entry.key)
onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
@ -149,19 +147,19 @@ private fun AddressBlock(
},
onOpenQrCodeClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onOpenQrCodeClick(entry.key)
onOpenQrCodeClick(address.value)
},
address = entry.value.value,
address = address.value,
primaryType = type,
)
SpacerH8()
}
}
ReceiveAddress.Type.Ens -> {
key(entry.key) {
key(address.value) {
EnsItem(
onCopyClick = {
onCopyClick(entry.key)
onCopyClick(address)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
@ -171,7 +169,7 @@ private fun AddressBlock(
)
}
},
address = entry.value.value,
address = address.value,
)
SpacerH8()
}
@ -334,9 +332,9 @@ private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<Recei
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"),
addresses = persistentListOf(
address,
address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
),
showMemoDisclaimer = false,
onCopyClick = {},

View file

@ -3,13 +3,12 @@ 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 addresses: ImmutableList<ReceiveAddress>,
val onOpenQrCodeClick: (address: String) -> Unit,
val onCopyClick: (address: ReceiveAddress) -> Unit,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
)

View file

@ -4,12 +4,11 @@ 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 addresses: ImmutableList<ReceiveAddress>,
val isEnsResultLoading: Boolean,
val notificationConfigs: ImmutableList<NotificationUM>,
)