Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-20 12:45:49 +02:00
parent 39a190f056
commit f0de027151
23 changed files with 1050 additions and 2 deletions

View file

@ -154,4 +154,10 @@ internal object NFTDomainModule {
): ObserveAndClearNFTCacheIfNeedUseCase {
return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository)
}
@Provides
@Singleton
fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase {
return GetNFTCurrencyUseCase(nftRepository)
}
}

View file

@ -273,6 +273,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
iconResId: Int = R.drawable.ic_alert_circle_24,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
iconTint: NotificationConfig.IconTint = NotificationConfig.IconTint.Unspecified,
) : NotificationUM(
config = NotificationConfig(
title = title,
@ -280,6 +281,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
iconTint = iconTint,
),
)

View file

@ -50,5 +50,9 @@
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
},
{
"name": "NEW_TOKEN_RECEIVE_ENABLED",
"version": "undefined"
}
]

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.res.TangemTheme
@ -28,11 +29,16 @@ import com.tangem.core.ui.utils.getGreyScaleColorFilter
* @param shouldDisplayNetwork specifies whether to display network badge
*/
@Composable
fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) {
fun CurrencyIcon(
state: CurrencyIconState,
modifier: Modifier = Modifier,
shouldDisplayNetwork: Boolean = true,
iconSize: Dp = 36.dp,
) {
BaseContainer(modifier = modifier) {
val iconModifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size36)
.size(iconSize)
when (state) {
is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier)

View file

@ -0,0 +1,103 @@
package com.tangem.core.ui.components.currency.icon
import androidx.annotation.DrawableRes
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.models.currency.CryptoCurrency
object CurrencyIconStateBuilder {
fun build(
cryptoCurrency: CryptoCurrency,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState = when (cryptoCurrency) {
is CryptoCurrency.Coin -> fromCoin(cryptoCurrency, isGrayscale, showCustomBadge)
is CryptoCurrency.Token -> fromToken(cryptoCurrency, isGrayscale, showCustomBadge)
}
private fun createCoinIcon(
url: String? = null,
@DrawableRes fallbackResId: Int = R.drawable.ic_empty_64,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = false,
): CurrencyIconState.CoinIcon = CurrencyIconState.CoinIcon(
url = url,
fallbackResId = fallbackResId,
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
private fun createTokenIcon(
url: String? = null,
@DrawableRes topBadgeIconResId: Int? = null,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = false,
fallbackTint: Color = Color.Black,
fallbackBackground: Color = Color.White,
): CurrencyIconState.TokenIcon = CurrencyIconState.TokenIcon(
url = url,
topBadgeIconResId = topBadgeIconResId,
isGrayscale = isGrayscale,
fallbackTint = fallbackTint,
fallbackBackground = fallbackBackground,
showCustomBadge = showCustomBadge,
)
private fun createCustomTokenIcon(
tint: Color,
background: Color,
@DrawableRes topBadgeIconResId: Int,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState.CustomTokenIcon = CurrencyIconState.CustomTokenIcon(
tint = tint,
background = background,
topBadgeIconResId = topBadgeIconResId,
isGrayscale = isGrayscale,
showCustomBadge = showCustomBadge,
)
private fun fromCoin(
coin: CryptoCurrency.Coin,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState.CoinIcon = createCoinIcon(
url = coin.iconUrl,
fallbackResId = coin.networkIconResId,
isGrayscale = isGrayscale || coin.network.isTestnet,
showCustomBadge = coin.isCustom && showCustomBadge,
)
private fun fromToken(
token: CryptoCurrency.Token,
isGrayscale: Boolean = false,
showCustomBadge: Boolean = true,
): CurrencyIconState {
val grayScale = isGrayscale || token.network.isTestnet
val background = token.tryGetBackgroundForTokenIcon(grayScale)
val tint = getTintForTokenIcon(background)
return if (token.isCustom && token.iconUrl == null) {
createCustomTokenIcon(
tint = tint,
background = background,
topBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
showCustomBadge = showCustomBadge,
)
} else {
createTokenIcon(
url = token.iconUrl,
topBadgeIconResId = token.networkIconResId,
isGrayscale = grayScale,
fallbackTint = tint,
fallbackBackground = background,
showCustomBadge = token.isCustom && showCustomBadge,
)
}
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M21.099,15C21.099,13.56 21.098,12.571 20.998,11.828C20.901,11.111 20.729,10.759 20.485,10.514C20.241,10.27 19.889,10.098 19.172,10.002C18.429,9.902 17.439,9.9 16,9.9H15C13.56,9.9 12.571,9.902 11.828,10.002C11.111,10.098 10.759,10.27 10.514,10.514C10.27,10.759 10.098,11.111 10.002,11.828C9.902,12.571 9.9,13.56 9.9,15V16C9.9,17.44 9.902,18.429 10.002,19.172C10.098,19.889 10.27,20.241 10.514,20.485C10.759,20.729 11.111,20.902 11.828,20.998C12.571,21.098 13.56,21.1 15,21.1H16C17.439,21.1 18.429,21.098 19.172,20.998C19.889,20.902 20.241,20.729 20.485,20.485C20.729,20.241 20.901,19.889 20.998,19.172C21.098,18.429 21.099,17.44 21.099,16V15ZM22.9,16C22.9,17.389 22.902,18.521 22.782,19.412C22.659,20.329 22.392,21.123 21.757,21.758C21.123,22.392 20.328,22.659 19.412,22.782C18.521,22.902 17.389,22.9 16,22.9H15C13.611,22.9 12.479,22.902 11.588,22.782C10.671,22.659 9.876,22.392 9.242,21.758C8.608,21.123 8.341,20.329 8.217,19.412C8.098,18.521 8.099,17.389 8.099,16V15C8.099,13.611 8.098,12.479 8.217,11.588C8.341,10.671 8.608,9.876 9.242,9.242C9.876,8.608 10.671,8.341 11.588,8.218C12.479,8.098 13.611,8.099 15,8.099H16C17.389,8.099 18.521,8.098 19.412,8.218C20.328,8.341 21.123,8.608 21.757,9.242C22.392,9.876 22.659,10.671 22.782,11.588C22.902,12.479 22.9,13.611 22.9,15V16Z"
android:fillColor="#919191"/>
<path
android:pathData="M2.099,9.999V8.999L2.099,8.945C2.099,7.581 2.099,6.466 2.217,5.587C2.34,4.67 2.607,3.876 3.241,3.241C3.876,2.607 4.67,2.34 5.587,2.217C6.466,2.099 7.581,2.099 8.945,2.099L8.999,2.099H9.999L10.054,2.099C11.417,2.099 12.532,2.099 13.411,2.217C14.328,2.34 15.123,2.607 15.757,3.241C15.978,3.463 16.16,3.709 16.307,3.983C16.542,4.42 16.378,4.966 15.941,5.201C15.503,5.437 14.957,5.273 14.722,4.835C14.656,4.713 14.579,4.608 14.484,4.514C14.24,4.27 13.888,4.098 13.171,4.001C12.428,3.901 11.439,3.9 9.999,3.9H8.999C7.559,3.9 6.57,3.901 5.827,4.001C5.11,4.098 4.758,4.27 4.514,4.514C4.269,4.758 4.097,5.11 4.001,5.827C3.901,6.57 3.899,7.559 3.899,8.999V9.999C3.899,11.439 3.901,12.428 4.001,13.171C4.097,13.888 4.269,14.24 4.514,14.484C4.692,14.663 4.923,14.797 5.292,14.895C5.772,15.022 6.059,15.515 5.932,15.995C5.812,16.446 5.371,16.726 4.921,16.654L4.831,16.635L4.606,16.569C4.09,16.404 3.633,16.149 3.241,15.757C2.607,15.123 2.34,14.328 2.217,13.411C2.099,12.532 2.099,11.417 2.099,10.054L2.099,9.999Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,38 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="36dp"
android:height="37dp"
android:viewportWidth="36"
android:viewportHeight="37">
<path
android:pathData="M0,18.658C0,8.717 8.059,0.658 18,0.658C27.941,0.658 36,8.717 36,18.658C36,28.599 27.941,36.658 18,36.658C8.059,36.658 0,28.599 0,18.658Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="7.08"
android:startY="6.638"
android:endX="38.587"
android:endY="33.361"
android:type="linear">
<item android:offset="0" android:color="#FF8BA0FF"/>
<item android:offset="0.524" android:color="#FF65B1FF"/>
<item android:offset="1" android:color="#FF91D0FF"/>
</gradient>
</aapt:attr>
</path>
<group>
<clip-path
android:pathData="M9.314,8.766h17.372v19.784h-17.372z"/>
<path
android:pathData="M17.55,9.187L12.219,17.958C12.177,18.027 12.08,18.035 12.028,17.973C11.559,17.416 9.811,15.045 11.974,12.885C13.948,10.913 16.463,9.507 17.395,9.021C17.501,8.966 17.612,9.085 17.55,9.187Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M17.255,28.285C17.362,28.36 17.493,28.233 17.422,28.124C16.231,26.313 12.273,20.287 11.726,19.383C11.187,18.491 10.126,17.008 10.038,15.739C10.029,15.613 9.854,15.587 9.809,15.706C9.738,15.898 9.663,16.127 9.592,16.389C8.702,19.695 9.995,23.203 12.802,25.168L17.255,28.285Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M17.988,28.129L23.319,19.358C23.36,19.289 23.458,19.281 23.509,19.343C23.979,19.9 25.727,22.271 23.564,24.431C21.589,26.403 19.075,27.809 18.143,28.295C18.037,28.35 17.926,28.231 17.988,28.129Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M18.288,9.03C18.182,8.955 18.051,9.082 18.122,9.191C19.313,11.002 23.271,17.028 23.818,17.932C24.357,18.824 25.418,20.307 25.506,21.576C25.515,21.702 25.69,21.728 25.734,21.609C25.805,21.417 25.881,21.188 25.951,20.926C26.841,17.62 25.549,14.112 22.742,12.147L18.288,9.03Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.816,11.215C3.377,11.215 2.667,10.515 2.667,9.036V4.846C2.667,3.367 3.377,2.667 4.816,2.667H9.066C10.505,2.667 11.215,3.367 11.215,4.846V9.036C11.215,10.515 10.505,11.215 9.066,11.215H4.816ZM14.934,11.215C13.485,11.215 12.785,10.515 12.785,9.036V4.846C12.785,3.367 13.485,2.667 14.934,2.667H19.184C20.624,2.667 21.333,3.367 21.333,4.846V9.036C21.333,10.515 20.624,11.215 19.184,11.215H14.934ZM4.776,9.635H9.096C9.455,9.635 9.635,9.455 9.635,9.086V4.796C9.635,4.426 9.455,4.246 9.096,4.246H4.776C4.416,4.246 4.246,4.426 4.246,4.796V9.086C4.246,9.455 4.416,9.635 4.776,9.635ZM14.894,9.635H19.224C19.584,9.635 19.754,9.455 19.754,9.086V4.796C19.754,4.426 19.584,4.246 19.224,4.246H14.894C14.535,4.246 14.355,4.426 14.355,4.796V9.086C14.355,9.455 14.535,9.635 14.894,9.635ZM6.046,8.046C5.886,8.046 5.836,7.976 5.836,7.796V6.076C5.836,5.906 5.886,5.836 6.046,5.836H7.826C7.976,5.836 8.046,5.906 8.046,6.076V7.796C8.046,7.976 7.976,8.046 7.826,8.046H6.046ZM16.204,8.046C16.054,8.046 16.004,7.976 16.004,7.796V6.076C16.004,5.906 16.054,5.836 16.204,5.836H17.984C18.144,5.836 18.204,5.906 18.204,6.076V7.796C18.204,7.976 18.144,8.046 17.984,8.046H16.204ZM4.816,21.333C3.377,21.333 2.667,20.633 2.667,19.144V14.965C2.667,13.485 3.377,12.785 4.816,12.785H9.066C10.505,12.785 11.215,13.485 11.215,14.965V19.144C11.215,20.633 10.505,21.333 9.066,21.333H4.816ZM13.475,15.454C13.325,15.454 13.275,15.384 13.275,15.204V13.475C13.275,13.315 13.325,13.245 13.475,13.245H15.254C15.414,13.245 15.474,13.315 15.474,13.475V15.204C15.474,15.384 15.414,15.454 15.254,15.454H13.475ZM18.834,15.454C18.674,15.454 18.624,15.384 18.624,15.204V13.475C18.624,13.315 18.674,13.245 18.834,13.245H20.614C20.764,13.245 20.823,13.315 20.823,13.475V15.204C20.823,15.384 20.764,15.454 20.614,15.454H18.834ZM4.776,19.754H9.096C9.455,19.754 9.635,19.574 9.635,19.204V14.915C9.635,14.535 9.455,14.365 9.096,14.365H4.776C4.416,14.365 4.246,14.535 4.246,14.915V19.204C4.246,19.574 4.416,19.754 4.776,19.754ZM16.174,18.124C16.024,18.124 15.974,18.064 15.974,17.884V16.154C15.974,15.984 16.024,15.924 16.174,15.924H17.964C18.114,15.924 18.174,15.984 18.174,16.154V17.884C18.174,18.064 18.114,18.124 17.964,18.124H16.174ZM6.046,18.154C5.886,18.154 5.836,18.094 5.836,17.914V16.184C5.836,16.014 5.886,15.954 6.046,15.954H7.826C7.976,15.954 8.046,16.014 8.046,16.184V17.914C8.046,18.094 7.976,18.154 7.826,18.154H6.046ZM13.475,20.803C13.325,20.803 13.275,20.733 13.275,20.553V18.834C13.275,18.664 13.325,18.594 13.475,18.594H15.254C15.414,18.594 15.474,18.664 15.474,18.834V20.553C15.474,20.733 15.414,20.803 15.254,20.803H13.475ZM18.834,20.803C18.674,20.803 18.624,20.733 18.624,20.553V18.834C18.624,18.664 18.674,18.594 18.834,18.594H20.614C20.764,18.594 20.823,18.664 20.823,18.834V20.553C20.823,20.733 20.764,20.803 20.614,20.803H18.834Z"
android:fillColor="#919191"/>
</vector>

View file

@ -0,0 +1,37 @@
package com.tangem.domain.models
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class TokenReceiveConfig(
val shouldShowWarning: Boolean,
val cryptoCurrency: CryptoCurrency,
val userWalletId: UserWalletId,
val showMemoDisclaimer: Boolean,
val receiveAddress: List<ReceiveAddressModel>,
val tokenReceiveNotification: List<TokenReceiveNotification> = emptyList(),
val asset: Asset = Asset.Currency,
)
@Serializable
data class ReceiveAddressModel(
val nameService: NameService,
val value: String,
val displayName: String,
) {
enum class NameService {
Default, Ens
}
}
@Serializable
data class TokenReceiveNotification(
val title: Int,
val subtitle: Int,
)
enum class Asset {
Currency, NFT
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.nft
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.nft.repository.NFTRepository
class GetNFTCurrencyUseCase(private val nftRepository: NFTRepository) {
operator fun invoke(network: Network): CryptoCurrency = nftRepository.getNFTCurrency(network)
}

View file

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

View file

@ -44,11 +44,13 @@ dependencies {
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)
/** 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,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,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,28 @@
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.decompose.ComposableContentComponent
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
@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(qrCodeUM.addressValue) },
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: (String) -> 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,
)