Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-08 12:03:08 +03:00
commit 04c1325ca0
120 changed files with 3503 additions and 316 deletions

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsUM
@ -28,6 +29,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
val previewState = DetailsUM(
items = previewBlocks,
footer = previewFooter,
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
popBack = { /* no-op */ },
)

View file

@ -1,9 +1,11 @@
package com.tangem.features.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.ImmutableList
internal data class DetailsUM(
val items: ImmutableList<DetailsItemUM>,
val footer: DetailsFooterUM,
val selectFeedbackEmailTypeBSConfig: TangemBottomSheetConfig,
val popBack: () -> Unit,
)

View file

@ -0,0 +1,16 @@
package com.tangem.features.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.details.impl.R
internal data class SelectEmailFeedbackTypeBS(
val onOptionClick: (Option) -> Unit,
) : TangemBottomSheetConfigContent {
enum class Option(val text: TextReference) {
General(resourceReference(R.string.common_contact_tangem_support)),
Visa(resourceReference(R.string.common_contact_visa_support)),
}
}

View file

@ -7,17 +7,22 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.entity.DetailsUM
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
import com.tangem.features.details.utils.ItemsBuilder
import com.tangem.features.details.utils.SocialsBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -47,6 +52,7 @@ internal class DetailsModel @Inject constructor(
private val appStateHolder: ReduxStateHolder,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@ -84,6 +90,7 @@ internal class DetailsModel @Inject constructor(
socials = socialsBuilder.buildAll(),
appVersion = getAppVersion(),
),
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
popBack = router::pop,
),
)
@ -99,12 +106,88 @@ internal class DetailsModel @Inject constructor(
private fun sendFeedback() {
modelScope.launch {
val userWallets = getWalletsUseCase.invokeSync()
val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse
?: error("Selected wallet is null")
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(cardInfo = cardInfo))
val feedbackType = when {
userWallets.all { it.scanResponse.card.isVisa } -> FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
userWallets.all { it.scanResponse.card.isVisa.not() } -> FeedbackEmailType.DirectUserRequest(cardInfo)
else -> {
showFeedbackEmailTypeOptionBS(cardInfo)
return@launch
}
}
sendFeedbackEmailUseCase(feedbackType)
}
}
private fun showFeedbackEmailTypeOptionBS(selectedCardInfo: CardInfo) {
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig =
it.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
)
}
},
content = SelectEmailFeedbackTypeBS(
onOptionClick = { option ->
onEmailFeedbackTypeOptionSelected(
selectedCardInfo = selectedCardInfo,
option = option,
)
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig =
it.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
)
}
},
),
),
)
}
}
private fun onEmailFeedbackTypeOptionSelected(
selectedCardInfo: CardInfo,
option: SelectEmailFeedbackTypeBS.Option,
) {
modelScope.launch {
val feedbackType = when (option) {
SelectEmailFeedbackTypeBS.Option.General -> {
if (selectedCardInfo.isVisa.not()) {
FeedbackEmailType.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa.not() }?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.DirectUserRequest(cardInfo)
}
}
SelectEmailFeedbackTypeBS.Option.Visa -> {
if (selectedCardInfo.isVisa) {
FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa }?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
}
}
}
sendFeedbackEmailUseCase(feedbackType)
}
}

View file

@ -59,6 +59,8 @@ internal fun DetailsScreen(
userWalletListBlockContent = userWalletListBlockContent,
)
}
SelectFeedbackEmailTypeBottomSheet(state.selectFeedbackEmailTypeBSConfig)
}
@Composable

View file

@ -0,0 +1,58 @@
package com.tangem.features.details.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
import com.tangem.features.details.impl.R
@Composable
internal fun SelectFeedbackEmailTypeBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<SelectEmailFeedbackTypeBS>(
config = config,
titleText = resourceReference(R.string.common_choose_action),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: SelectEmailFeedbackTypeBS) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
SelectEmailFeedbackTypeBS.Option.entries.forEachIndexed { index, type ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = SelectEmailFeedbackTypeBS.Option.entries.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(type) },
showDivider = index != SelectEmailFeedbackTypeBS.Option.entries.lastIndex,
) {
InputRowChecked(
text = type.text,
checked = false,
)
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.nft.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
interface NFTAssetTraitsComponent : ComposableContentComponent {
data class Params(
val nftAsset: NFTAsset,
)
interface Factory : ComponentFactory<Params, NFTAssetTraitsComponent>
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.nft.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
interface NFTDetailsBlockComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
)
interface Factory : ComponentFactory<Params, NFTDetailsBlockComponent>
}

View file

@ -10,6 +10,7 @@ interface NFTDetailsComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
)
interface Factory : ComponentFactory<Params, NFTDetailsComponent>

View file

@ -20,7 +20,7 @@ internal class UpdateDataStateTransformer(
private val onRetryClick: () -> Unit,
private val onExpandCollectionClick: (NFTCollection) -> Unit,
private val onRetryAssetsClick: (NFTCollection) -> Unit,
private val onAssetClick: (NFTAsset) -> Unit,
private val onAssetClick: (NFTAsset, String) -> Unit,
private val initialSearchBarFactory: () -> SearchBarUM,
) : Transformer<NFTCollectionsStateUM> {
@ -105,12 +105,12 @@ internal class UpdateDataStateTransformer(
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
items = assets
.items
.map { it.transform() }
.map { it.transform(name.orEmpty()) }
.toPersistentList(),
)
}
private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM(
private fun NFTAsset.transform(collectionName: String): NFTCollectionAssetUM = NFTCollectionAssetUM(
id = id.toString(),
name = name.orEmpty(),
imageUrl = media?.url,
@ -121,7 +121,7 @@ internal class UpdateDataStateTransformer(
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
},
onItemClick = {
onAssetClick(this)
onAssetClick(this, collectionName)
},
)

View file

@ -115,11 +115,12 @@ internal class NFTCollectionsModel @Inject constructor(
// TODO refresh all
}
private fun onAssetClick(asset: NFTAsset) {
private fun onAssetClick(asset: NFTAsset, collectionName: String) {
router.push(
AppRoute.NFTDetails(
userWalletId = params.userWalletId,
nftAsset = asset,
collectionName = collectionName,
),
)
}

View file

@ -2,7 +2,6 @@ package com.tangem.features.nft.collections.ui
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
@ -11,25 +10,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM
import com.tangem.features.nft.collections.entity.NFTCollectionUM
import com.tangem.features.nft.common.ui.NFTLogo
import com.tangem.features.nft.impl.R
import kotlinx.collections.immutable.persistentListOf
@ -52,7 +45,11 @@ internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier
),
verticalAlignment = Alignment.CenterVertically,
) {
Logo(state)
NFTLogo(
imageUrl = state.logoUrl,
networkIconId = state.networkIconId,
background = TangemTheme.colors.background.primary,
)
Text(state)
@ -81,46 +78,6 @@ internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier
}
}
@Composable
private fun Logo(state: NFTCollectionUM) {
val networkBadgeOffset = TangemTheme.dimens.spacing6
Box(
modifier = Modifier,
) {
SubcomposeAsyncImage(
modifier = Modifier
.align(Alignment.CenterStart)
.size(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(LocalContext.current)
.data(state.logoUrl)
.crossfade(true)
.build(),
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius8)
},
error = {
Box(
modifier = Modifier
.clip(shape = TangemTheme.shapes.roundedCorners8)
.background(TangemTheme.colors.field.primary),
)
},
contentScale = ContentScale.Crop,
contentDescription = null,
)
CurrencyIconTopBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
iconResId = state.networkIconId,
alpha = 1f,
colorFilter = null,
)
}
}
@Composable
private fun RowScope.Text(state: NFTCollectionUM) {
Column(

View file

@ -0,0 +1,60 @@
package com.tangem.features.nft.common.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun NFTLogo(imageUrl: String?, @DrawableRes networkIconId: Int, background: Color = Color.Transparent) {
val networkBadgeOffset = TangemTheme.dimens.spacing6
Box(
modifier = Modifier,
) {
SubcomposeAsyncImage(
modifier = Modifier
.align(Alignment.CenterStart)
.size(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.build(),
loading = {
RectangleShimmer(radius = TangemTheme.dimens.radius8)
},
error = {
Box(
modifier = Modifier
.clip(shape = TangemTheme.shapes.roundedCorners8)
.background(TangemTheme.colors.field.primary),
)
},
contentScale = ContentScale.Crop,
contentDescription = null,
)
CurrencyIconTopBadge(
modifier = Modifier
.offset(x = networkBadgeOffset, y = -networkBadgeOffset)
.align(Alignment.TopEnd),
iconResId = networkIconId,
alpha = 1f,
colorFilter = null,
background = background,
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.nft.details.block
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.nft.details.block.ui.NFTDetailsBlock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class DefaultNFTDetailsBlockComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTDetailsBlockComponent.Params,
) : NFTDetailsBlockComponent, AppComponentContext by context {
@Composable
override fun Content(modifier: Modifier) {
NFTDetailsBlock(
assetName = stringReference(params.nftAsset.name.orEmpty()),
collectionName = stringReference(params.nftCollectionName),
assetImage = params.nftAsset.media?.url,
networkIconRes = getActiveIconRes(params.nftAsset.network.id.value),
)
}
@AssistedFactory
interface Factory : NFTDetailsBlockComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTDetailsBlockComponent.Params,
): DefaultNFTDetailsBlockComponent
}
}

View file

@ -0,0 +1,84 @@
package com.tangem.features.nft.details.block.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.common.ui.NFTLogo
import com.tangem.features.nft.impl.R
@Composable
internal fun NFTDetailsBlock(
assetName: TextReference,
collectionName: TextReference,
assetImage: String?,
networkIconRes: Int,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.action)
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = "NFT Asset",
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
NFTLogo(
assetImage,
networkIconRes,
background = TangemTheme.colors.background.action,
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Text(
text = assetName.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = collectionName.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun NFTDetailsBlock_Preview() {
TangemThemePreview {
NFTDetailsBlock(
assetName = stringReference("NFT Asset Name"),
collectionName = stringReference("NFT Collection"),
assetImage = null,
networkIconRes = R.drawable.img_polygon_22,
)
}
}
// endregion

View file

@ -6,7 +6,7 @@ internal data class NFTDetailsUM(
val nftAsset: NFTAssetUM,
val onBackClick: () -> Unit,
val onReadMoreClick: () -> Unit,
val onSeeAllClick: () -> Unit,
val onSeeAllTraitsClick: () -> Unit,
val onExploreClick: () -> Unit,
val onSendClick: () -> Unit,
val bottomSheetConfig: TangemBottomSheetConfig?,

View file

@ -0,0 +1,9 @@
package com.tangem.features.nft.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
internal data class NFTInfoBottomSheetConfig(
val title: TextReference,
val text: TextReference,
) : TangemBottomSheetConfigContent

View file

@ -1,5 +1,6 @@
package com.tangem.features.nft.details.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -9,7 +10,8 @@ import com.tangem.features.nft.details.entity.NFTAssetUM
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
@ -35,7 +37,7 @@ internal class NFTDetailsModel @Inject constructor(
),
onBackClick = ::navigateBack,
onReadMoreClick = ::onReadMoreClick,
onSeeAllClick = ::onSeeAllClick,
onSeeAllTraitsClick = ::onSeeAllTraitsClick,
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
bottomSheetConfig = null,
@ -46,8 +48,12 @@ internal class NFTDetailsModel @Inject constructor(
// TODO implement
}
private fun onSeeAllClick() {
// TODO implement
private fun onSeeAllTraitsClick() {
router.push(
AppRoute.NFTAssetTraits(
nftAsset = params.nftAsset,
),
)
}
private fun onExploreClick() {
@ -56,6 +62,13 @@ internal class NFTDetailsModel @Inject constructor(
private fun onSendClick() {
// TODO implement
// router.push(
// AppRoute.NFTSend(
// userWalletId = params.userWalletId,
// nftAsset = params.nftAsset,
// nftCollectionName = params.nftCollectionName,
// ),
// )
}
private fun navigateBack() {

View file

@ -11,9 +11,12 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.details.entity.NFTDetailsUM
import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig
import com.tangem.features.nft.details.ui.bottomsheet.NFTInfoBottomSheet
import com.tangem.features.nft.impl.R
@Composable
@ -37,11 +40,12 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
NFTDetailsAsset(
state = state.nftAsset,
onReadMoreClick = state.onReadMoreClick,
onSeeAllClick = state.onSeeAllClick,
onSeeAllTraitsClick = state.onSeeAllTraitsClick,
onExploreClick = state.onExploreClick,
modifier = Modifier
.padding(innerPadding),
)
ShowBottomSheet(state.bottomSheetConfig)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
@ -54,4 +58,12 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) {
)
},
)
}
@Composable
fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig == null) return
when (bottomSheetConfig.content) {
is NFTInfoBottomSheetConfig -> NFTInfoBottomSheet(bottomSheetConfig)
}
}

View file

@ -24,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
internal fun NFTDetailsAsset(
state: NFTAssetUM,
onReadMoreClick: () -> Unit,
onSeeAllClick: () -> Unit,
onSeeAllTraitsClick: () -> Unit,
onExploreClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -72,7 +72,7 @@ internal fun NFTDetailsAsset(
NFTBlocksGroupAction(
text = resourceReference(R.string.common_see_all),
startIcon = { },
onClick = onSeeAllClick,
onClick = onSeeAllTraitsClick,
)
},
)
@ -103,7 +103,7 @@ private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetProvider::cla
NFTDetailsAsset(
state = state,
onReadMoreClick = { },
onSeeAllClick = { },
onSeeAllTraitsClick = { },
onExploreClick = { },
)
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.nft.details.ui.bottomsheet
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.details.entity.NFTInfoBottomSheetConfig
@Composable
fun NFTInfoBottomSheet(config: TangemBottomSheetConfig) {
val scrollState = rememberScrollState()
TangemBottomSheet<NFTInfoBottomSheetConfig>(
config = config,
title = { content ->
TangemBottomSheetTitle(title = content.title)
},
) { content ->
Column(
modifier = Modifier.verticalScroll(scrollState),
) {
Text(
text = content.text.resolveReference(),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
)
}
}
}
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_StakingInfoBottomSheet() {
TangemThemePreview {
NFTInfoBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = NFTInfoBottomSheetConfig(
title = stringReference("Title"),
text = stringReference(
"""
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula
tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat,
at bibendum eros lacinia. Proin hendrerit massa non velit congue,
in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada.
Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat.
Donec sagittis enim in metus lacinia, vel tempor nunc bibendum.
""".trimIndent(),
),
),
),
)
}
}

View file

@ -1,16 +1,23 @@
package com.tangem.features.nft
package com.tangem.features.nft.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.model.Model
import com.tangem.features.nft.DefaultNFTFeatureToggles
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.details.DefaultNFTDetailsComponent
import com.tangem.features.nft.details.block.DefaultNFTDetailsBlockComponent
import com.tangem.features.nft.details.model.NFTDetailsModel
import com.tangem.features.nft.receive.DefaultNFTReceiveComponent
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.traits.DefaultNFTAssetTraitsComponent
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -62,4 +69,19 @@ internal interface NFTFeatureModuleBinds {
@IntoMap
@ClassKey(NFTDetailsModel::class)
fun bindNFTDetailsModel(model: NFTDetailsModel): Model
@Binds
@Singleton
fun bindNFTDetailsBlockComponentFactory(
impl: DefaultNFTDetailsBlockComponent.Factory,
): NFTDetailsBlockComponent.Factory
@Binds
@Singleton
fun bindNFTTraitsComponentFactory(impl: DefaultNFTAssetTraitsComponent.Factory): NFTAssetTraitsComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTAssetTraitsModel::class)
fun bindNFTTraitsModel(model: NFTAssetTraitsModel): Model
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.nft.traits
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.traits.ui.NFTAssetTraits
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTAssetTraitsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTAssetTraitsComponent.Params,
) : NFTAssetTraitsComponent, AppComponentContext by context {
private val model: NFTAssetTraitsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
NFTAssetTraits(state)
}
@AssistedFactory
interface Factory : NFTAssetTraitsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTAssetTraitsComponent.Params,
): DefaultNFTAssetTraitsComponent
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.features.nft.traits.entity
data class NFTAssetTraitUM(
val id: String,
val name: String,
val value: String,
)

View file

@ -0,0 +1,8 @@
package com.tangem.features.nft.traits.entity
import kotlinx.collections.immutable.ImmutableList
data class NFTAssetTraitsUM(
val onBackClick: () -> Unit,
val traits: ImmutableList<NFTAssetTraitUM>,
)

View file

@ -0,0 +1,48 @@
package com.tangem.features.nft.traits.model
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.decompose.navigation.Router
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.features.nft.component.NFTAssetTraitsComponent
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class NFTAssetTraitsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
paramsContainer: ParamsContainer,
) : Model() {
private val params: NFTAssetTraitsComponent.Params = paramsContainer.require()
val state: StateFlow<NFTAssetTraitsUM> get() = _state
private val _state = MutableStateFlow(
value = NFTAssetTraitsUM(
traits = params.nftAsset.transform(),
onBackClick = ::navigateBack,
),
)
private fun NFTAsset.transform(): ImmutableList<NFTAssetTraitUM> = this
.traits
.mapIndexed { index, trait ->
NFTAssetTraitUM(
id = index.toString(),
name = trait.name,
value = trait.value,
)
}.toPersistentList()
private fun navigateBack() {
router.pop()
}
}

View file

@ -0,0 +1,29 @@
package com.tangem.features.nft.traits.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
@Composable
internal fun NFTAssetTrait(state: NFTAssetTraitUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
Text(
text = state.name,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing4),
text = state.value,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.nft.traits.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
@Composable
internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
modifier = modifier,
containerColor = TangemTheme.colors.background.secondary,
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_back_24,
onIconClicked = state.onBackClick,
),
title = stringResourceSafe(R.string.nft_traits_title),
)
},
content = { innerPadding ->
NFTAssetTraitsContent(
modifier = Modifier
.padding(innerPadding),
state = state,
)
},
)
}

View file

@ -0,0 +1,94 @@
package com.tangem.features.nft.traits.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.block.information.InformationBlock
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.nft.traits.entity.NFTAssetTraitUM
import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun NFTAssetTraitsContent(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) {
InformationBlock(
modifier = modifier
.padding(TangemTheme.dimens.spacing16),
title = null,
contentHorizontalPadding = 0.dp,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
) {
state.traits.forEach { trait ->
key(trait.id) {
NFTAssetTrait(
modifier = Modifier
.padding(
horizontal = TangemTheme.dimens.spacing12,
vertical = TangemTheme.dimens.spacing8,
),
state = trait,
)
}
}
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetTraitsProvider::class) state: NFTAssetTraitsUM) {
TangemThemePreview {
NFTAssetTraitsContent(
state = state,
)
}
}
private class NFTAssetTraitsProvider : CollectionPreviewParameterProvider<NFTAssetTraitsUM>(
collection = listOf(
NFTAssetTraitsUM(
traits = persistentListOf(
NFTAssetTraitUM(
id = "1",
name = "Trait 1",
value = "Value",
),
NFTAssetTraitUM(
id = "2",
name = "Trait 2",
value = "Value",
),
NFTAssetTraitUM(
id = "3",
name = "Trait 3",
value = "Value",
),
NFTAssetTraitUM(
id = "4",
name = "Trait 4",
value = "Value",
),
NFTAssetTraitUM(
id = "5",
name = "Trait 5",
value = "Value",
),
),
onBackClick = { },
),
),
)

View file

@ -9,6 +9,7 @@ import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -40,7 +41,13 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
componentScope.launch {
val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
sendFeedbackEmailUseCase(
if (params.scanResponse.card.isVisa) {
FeedbackEmailType.Visa.Activation(cardInfo)
} else {
FeedbackEmailType.DirectUserRequest(cardInfo)
},
)
}
}

View file

@ -16,4 +16,5 @@ dependencies {
/** Domain models */
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.nft.models)
}

View file

@ -0,0 +1,17 @@
package com.tangem.features.send.v2.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
interface NFTSendComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
)
interface Factory : ComponentFactory<Params, NFTSendComponent>
}

View file

@ -2,9 +2,11 @@ package com.tangem.features.send.v2.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.DefaultSendFeatureToggles
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.send.DefaultSendComponent
import com.tangem.features.send.v2.sendnft.DefaultNFTSendComponent
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -29,4 +31,8 @@ internal interface SendFeatureModuleBinds {
@Binds
@Singleton
fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory
@Binds
@Singleton
fun provideNFTSendComponentFactory(impl: DefaultNFTSendComponent.Factory): NFTSendComponent.Factory
}

View file

@ -2,8 +2,6 @@ package com.tangem.features.send.v2.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel
@ -18,11 +16,6 @@ import dagger.multibindings.IntoMap
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendAmountModel::class)
@ -38,11 +31,6 @@ internal interface SendModelModule {
@ClassKey(SendFeeModel::class)
fun provideSendFeeModel(model: SendFeeModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
@Binds
@IntoMap
@ClassKey(NotificationsModel::class)

View file

@ -0,0 +1,26 @@
package com.tangem.features.send.v2.send.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.model.SendModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.send.v2.sendnft
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTSendComponent.Params,
) : NFTSendComponent, AppComponentContext by appComponentContext {
// private val stackNavigation = StackNavigation<NFTSendRoute>()
//
// private val innerRouter = InnerRouter<NFTSendRoute>(
// stackNavigation = stackNavigation,
// popCallback = { onChildBack() },
// )
// private val initialRoute = NFTSendRoute.Empty
// private val currentRouteFlow = MutableStateFlow<NFTSendRoute>(initialRoute)
private val model: NFTSendModel = getOrCreateModel(params = params/*, router = innerRouter*/)
// private val childStack = childStack(
// key = "NFTSendInnerStack",
// source = stackNavigation,
// serializer = null,
// initialConfiguration = initialRoute,
// handleBackButton = true,
// childFactory = { configuration, factoryContext ->
// // todo
// ComposableContentComponent { }
// },
// )
init {
// childStack.subscribe(
// lifecycle = lifecycle,
// mode = ObserveLifecycleMode.CREATE_DESTROY,
// ) { stack ->
// componentScope.launch {
// when (val activeComponent = stack.active.instance) {
// is SendDestinationComponent -> {
// // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened)
// activeComponent.updateState(model.uiState.value.destinationUM)
// }
// is SendFeeComponent -> {
// // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened)
// }
// }
// currentRouteFlow.emit(stack.active.configuration)
// }
// }
}
@Composable
override fun Content(modifier: Modifier) {
// val stackState by childStack.subscribeAsState()
// val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = ::onChildBack)
// TODO
}
private fun onChildBack() {
// TODO
// val isEmptyRoute = childStack.value.active.configuration == NFTSendRoute.Empty
// val isEmptyStack = childStack.value.backStack.isEmpty()
// val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success
//
// if (isEmptyRoute || isEmptyStack || isSuccess) {
// router.pop()
// } else {
// stackNavigation.pop()
// }
}
@AssistedFactory
interface Factory : NFTSendComponent.Factory {
override fun create(context: AppComponentContext, params: NFTSendComponent.Params): DefaultNFTSendComponent
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.send.v2.sendnft.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface NFTSendModelModule {
@Binds
@IntoMap
@ClassKey(NFTSendModel::class)
fun provideNFTSendModel(model: NFTSendModel): Model
}

View file

@ -0,0 +1,144 @@
package com.tangem.features.send.v2.sendnft.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
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.decompose.navigation.Router
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
internal interface SendNFTComponentCallback :
SendFeeComponent.ModelCallback,
SendDestinationComponent.ModelCallback
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class NFTSendModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
) : Model(), SendNFTComponentCallback {
val params: NFTSendComponent.Params = paramsContainer.require()
private val userWalletId = params.userWalletId
private val _uiState = MutableStateFlow(initialState())
val uiState = _uiState.asStateFlow()
private val _isBalanceHiddenFlow = MutableStateFlow(false)
val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow()
var cryptoCurrency: CryptoCurrency by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
init {
subscribeOnCurrencyStatusUpdates()
initAppCurrency()
}
override fun onNavigationResult(navigationUM: NavigationUM) {
_uiState.update { it.copy(navigationUM = navigationUM) }
}
override fun onDestinationResult(destinationUM: DestinationUM) {
_uiState.update { it.copy(destinationUM = destinationUM) }
// todo
}
override fun onFeeResult(feeUM: FeeUM) {
_uiState.update { it.copy(feeUM = feeUM) }
router.pop()
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun subscribeOnCurrencyStatusUpdates() {
modelScope.launch {
getUserWalletUseCase(params.userWalletId).fold(
ifRight = { wallet ->
userWallet = wallet
// cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId).getOrNull()
// ?.filterIsInstance<CryptoCurrency.Coin>()
// ?.firstOrNull { it.network == nftAsset.network }
// ?: return@launch
getCurrenciesStatusUpdates(
isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick)
return@launch
},
)
}
}
private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
isSingleWalletWithTokens = isSingleWalletWithToken,
).onEach { maybeCryptoCurrency ->
maybeCryptoCurrency.fold(
ifRight = { cryptoStatus ->
cryptoCurrencyStatus = cryptoStatus
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWalletId = userWalletId,
cryptoCurrencyStatus = cryptoStatus,
).getOrNull() ?: cryptoStatus
// router.push(NFTSendRoute.Destination(isEditMode = false))
},
ifLeft = {
// sendConfirmAlertFactory.getGenericErrorState {
// onFailedTxEmailClick(it.toString())
// }
},
)
}.launchIn(modelScope)
}
private fun initialState(): NFTSendUM = NFTSendUM(
destinationUM = DestinationUM.Empty(),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
)
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.send.v2.sendnft.ui.state
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
internal data class NFTSendUM(
val destinationUM: DestinationUM,
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
)

View file

@ -2,8 +2,13 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.visa.GetVisaCurrencyUseCase
import com.tangem.domain.visa.GetVisaTxDetailsUseCase
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.BalancesAndLimitsBottomSheetConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.VisaTxDetailsBottomSheetConverter
@ -20,14 +25,20 @@ internal interface VisaWalletIntents {
fun onVisaTransactionClick(id: String)
fun onExploreClick(exploreUrl: String)
fun onDisputeClick(txDetails: VisaTxDetails)
}
@Suppress("LongParameterList")
@ModelScoped
internal class VisaWalletIntentsImplementor @Inject constructor(
private val stateController: WalletStateController,
private val eventSender: WalletEventSender,
private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase,
private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) : BaseWalletClickIntents(), VisaWalletIntents {
@ -78,4 +89,20 @@ internal class VisaWalletIntentsImplementor @Inject constructor(
override fun onExploreClick(exploreUrl: String) {
router.openUrl(exploreUrl)
}
override fun onDisputeClick(txDetails: VisaTxDetails) {
modelScope.launch {
val userWalletId = stateController.getSelectedWalletId()
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull { it.walletId == userWalletId } ?: return@launch
val cardInfo = getCardInfoUseCase.invoke(userWallet.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(
FeedbackEmailType.Visa.Dispute(
cardInfo = cardInfo,
visaTxDetails = txDetails,
),
)
}
}
}

View file

@ -6,6 +6,7 @@ import kotlinx.collections.immutable.ImmutableList
internal data class VisaTxDetailsBottomSheetConfig(
val transaction: Transaction,
val requests: ImmutableList<Request>,
val onDisputeClick: () -> Unit,
) : TangemBottomSheetConfigContent {
data class Transaction(

View file

@ -24,6 +24,7 @@ internal class VisaTxDetailsBottomSheetConverter(
return VisaTxDetailsBottomSheetConfig(
transaction = createTransaction(value),
requests = value.requests.map(::createRequest).toImmutableList(),
onDisputeClick = { clickIntents.onDisputeClick(value) },
)
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -15,6 +16,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -36,26 +38,45 @@ internal fun VisaTxDetailsBottomSheet(config: TangemBottomSheetConfig) {
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun VisaTxDetailsBottomSheetContent(config: VisaTxDetailsBottomSheetConfig, modifier: Modifier = Modifier) {
ContentContainer(
modifier = modifier,
blocksCount = config.requests.size.inc(),
title = {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size44)
.background(TangemTheme.colors.background.secondary),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(R.string.visa_transaction_details_header),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
},
block = { index ->
if (index == 0) {
}
LazyColumn(
modifier = modifier.background(TangemTheme.colors.background.secondary),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16,
),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
TransactionBlock(config.transaction)
} else {
BlockchainRequestBlock(config.requests[index - 1])
}
},
)
items(config.requests) { item ->
BlockchainRequestBlock(item)
}
item {
DisputeButton(config.onDisputeClick)
}
}
}
}
@Composable
@ -173,38 +194,14 @@ private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Reque
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ContentContainer(
blocksCount: Int,
title: @Composable BoxScope.() -> Unit,
block: @Composable ColumnScope.(Int) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.background(TangemTheme.colors.background.secondary),
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16,
),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
) {
stickyHeader {
Box(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size44)
.background(TangemTheme.colors.background.secondary),
contentAlignment = Alignment.Center,
content = title,
)
}
items(blocksCount) { index ->
Column {
block(index)
}
}
}
private fun DisputeButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
SecondaryButtonIconStart(
modifier = modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.visa_tx_dispute_button),
iconResId = R.drawable.ic_alert_triangle_20,
onClick = onClick,
)
}
// region Preview
@ -249,20 +246,8 @@ private class VisaTxDetailsBottomSheetParameterProvider :
txStatus = "confirmed",
onExploreClick = {},
),
VisaTxDetailsBottomSheetConfig.Request(
id = "524582128501966799",
type = "settlement",
status = "accepted",
blockchainAmount = "1.0614 USDT",
transactionAmount = "0.99 €",
currencyCode = "978",
errorCode = 0,
date = "2023-12-01 00:01:00.000 +0300",
txHash = "0x635841d5fbdf1087cdd929019c863ee88a7165e4340bc17ddd0b1d04dfb11daa",
txStatus = "confirmed",
onExploreClick = {},
),
),
onDisputeClick = {},
),
),
)