diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index dcedc27851..4d82335720 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -154,4 +154,10 @@ internal object NFTDomainModule { ): ObserveAndClearNFTCacheIfNeedUseCase { return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) } + + @Provides + @Singleton + fun provideGetNftCurrencyUseCase(nftRepository: NFTRepository): GetNFTCurrencyUseCase { + return GetNFTCurrencyUseCase(nftRepository) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index ff58fd1c80..1d4ff127d6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -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, ), ) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6ecb2f5ac6..1158354430 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -50,5 +50,9 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NEW_TOKEN_RECEIVE_ENABLED", + "version": "undefined" } ] diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 4727ba9759..ff97530009 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -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) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt new file mode 100644 index 0000000000..1aaed64244 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconStateBuilder.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_copy_new_24.xml b/core/ui/src/main/res/drawable/ic_copy_new_24.xml new file mode 100644 index 0000000000..a073a2b686 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_copy_new_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_ens_36.xml b/core/ui/src/main/res/drawable/ic_ens_36.xml new file mode 100644 index 0000000000..29b19ea2c7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_ens_36.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml new file mode 100644 index 0000000000..173a8c1979 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_qrcode_new_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt new file mode 100644 index 0000000000..728496ef05 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt @@ -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, + val tokenReceiveNotification: List = 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 +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt new file mode 100644 index 0000000000..85f77d810a --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCurrencyUseCase.kt @@ -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) +} \ No newline at end of file diff --git a/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..62175072dd --- /dev/null +++ b/features/token-recieve/api/src/main/kotlin/com/tangem/features/tokenreceive/TokenReceiveFeatureToggle.kt @@ -0,0 +1,6 @@ +package com.tangem.features.tokenreceive + +interface TokenReceiveFeatureToggle { + + val isNewTokenReceiveEnabled: Boolean +} \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index d46cd33aad..dfd0d93ada 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -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) diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt new file mode 100644 index 0000000000..209317d97b --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/DefaultTokenReceiveFeatureToggle.kt @@ -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") +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt new file mode 100644 index 0000000000..960dda1224 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt new file mode 100644 index 0000000000..94415b927d --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/ContainerWithSnackbarHost.kt @@ -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), + ) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt new file mode 100644 index 0000000000..10f41e7e05 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt @@ -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, + 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, + 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 { + 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 + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt new file mode 100644 index 0000000000..5571b27657 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveContent.kt @@ -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, + modifier: Modifier = Modifier, +) { + Children( + stack = stackState, + animation = stackAnimation(fade()), + modifier = modifier + .fillMaxSize() + .animateContentSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt new file mode 100644 index 0000000000..2eb6310cbd --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveQrCodeContent.kt @@ -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 { + private val config = QrCodeUM( + network = "Ethereum", + addressName = stringReference("Etherium"), + addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", + onCopyClick = {}, + onShareClick = {}, + ) + + override val values: Sequence + get() = sequenceOf(config) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt new file mode 100644 index 0000000000..2aa8aeff9c --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveWarningContent.kt @@ -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 { + val iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = null, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + showCustomBadge = false, + ) + + override val values: Sequence + get() = sequenceOf( + WarningUM( + iconState = iconState, + onWarningAcknowledged = {}, + network = "Etherium", + networkIcon = R.drawable.ic_eth_16, + ), + ) +} \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt new file mode 100644 index 0000000000..e3c4f5adf0 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/QrCodeUM.kt @@ -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, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt new file mode 100644 index 0000000000..3953389152 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/ReceiveAssetsUM.kt @@ -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, + val onOpenQrCodeClick: (id: Int) -> Unit, + val onCopyClick: (id: Int) -> Unit, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, + val fullName: String, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt new file mode 100644 index 0000000000..27eecde6a3 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/TokenReceiveUM.kt @@ -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, + val isEnsResultLoading: Boolean, + val notificationConfigs: ImmutableList, +) \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt new file mode 100644 index 0000000000..c31262fd58 --- /dev/null +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/state/WarningUM.kt @@ -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, +) \ No newline at end of file