Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-16 10:57:01 +03:00
commit 3c2a006fb9
331 changed files with 8261 additions and 1670 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

@ -29,7 +29,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -38,7 +37,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -47,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
onClick = {},
),

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

@ -18,19 +18,23 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.impl.R
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
@ -39,8 +43,11 @@ internal class UserWalletsFetcher @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
) {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets)
@ -52,12 +59,14 @@ internal class UserWalletsFetcher @Inject constructor(
flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(),
flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances ->
flow4 = loadArtworks(wallets),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks ->
val models = createUiModels(
wallets = wallets,
maybeAppCurrency = maybeAppCurrency,
maybeBalances = maybeBalances,
balanceHidingSettings = balanceHidingSettings,
artworks = artworks,
).getOrElse(
ifLoading = { return@combine },
ifError = {
@ -72,11 +81,27 @@ internal class UserWalletsFetcher @Inject constructor(
}.collect()
}
private fun loadArtworks(wallets: List<UserWallet>): Flow<HashMap<UserWalletId, ArtworkModel>> {
return flow {
emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish
wallets.forEach { wallet ->
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
size = ArtworkSize.SMALL,
)
loadedArtworks[wallet.walletId] = artwork
emit(loadedArtworks)
}
}
}
private fun createUiModels(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
artworks: HashMap<UserWalletId, ArtworkModel>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
@ -99,6 +124,7 @@ internal class UserWalletsFetcher @Inject constructor(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
)
.convert(userWallet)
}

View file

@ -27,7 +27,6 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
name = stringReference("Wallet 1"),
information = stringReference("3 cards"),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,
onClick = {},

View file

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

View file

@ -4,11 +4,11 @@ import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface NFTReceiveComponent : ComposableContentComponent {
interface NFTComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, NFTReceiveComponent>
interface Factory : ComponentFactory<Params, NFTComponent>
}

View file

@ -5,12 +5,13 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
interface NFTDetailsComponent : ComposableContentComponent {
interface NFTDetailsBlockComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
)
interface Factory : ComponentFactory<Params, NFTDetailsComponent>
interface Factory : ComponentFactory<Params, NFTDetailsBlockComponent>
}

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.core.datasource)
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.nft)
implementation(projects.domain.nft.models)
implementation(projects.domain.tokens.models)

View file

@ -6,17 +6,18 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.collections.ui.NFTCollections
import com.tangem.features.nft.component.NFTCollectionsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTCollectionsComponent @AssistedInject constructor(
internal class NFTCollectionsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTCollectionsComponent.Params,
) : NFTCollectionsComponent, AppComponentContext by context {
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: NFTCollectionsModel = getOrCreateModel(params)
@ -27,11 +28,10 @@ internal class DefaultNFTCollectionsComponent @AssistedInject constructor(
NFTCollections(state, modifier)
}
@AssistedFactory
interface Factory : NFTCollectionsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTCollectionsComponent.Params,
): DefaultNFTCollectionsComponent
}
data class Params(
val userWalletId: UserWalletId,
val onBackClick: () -> Unit,
val onAssetClick: (asset: NFTAsset, collectionName: String) -> Unit,
val onReceiveClick: () -> Unit,
)
}

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> {
@ -61,7 +61,25 @@ internal class UpdateDataStateTransformer(
state: NFTCollectionsStateUM,
query: String,
): ImmutableList<NFTCollectionUM> = mapNotNull {
if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) {
val assetsFulfillQuery = if (query.isEmpty()) {
true
} else {
when (val assets = it.assets) {
is NFTCollection.Assets.Empty,
is NFTCollection.Assets.Failed,
is NFTCollection.Assets.Loading,
-> true
is NFTCollection.Assets.Value -> {
assets.items.any { asset ->
asset.name?.lowercase()?.contains(query.lowercase()) == true
}
}
}
}
val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true
if (collectionFulfillQuery || assetsFulfillQuery) {
NFTCollectionUM(
id = it.id.toString(),
networkIconId = getActiveIconRes(it.network.id.value),
@ -105,12 +123,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 +139,7 @@ internal class UpdateDataStateTransformer(
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
},
onItemClick = {
onAssetClick(this)
onAssetClick(this, collectionName)
},
)

View file

@ -1,24 +1,21 @@
package com.tangem.features.nft.collections.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
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.fields.InputManager
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase
import com.tangem.domain.nft.GetNFTCollectionsUseCase
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.nft.models.NFTCollection
import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer
import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer
import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -29,23 +26,22 @@ import javax.inject.Inject
@ModelScoped
internal class NFTCollectionsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val searchManager: InputManager,
private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase,
private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase,
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<NFTCollectionsStateUM> get() = _state
private val params: NFTCollectionsComponent.Params = paramsContainer.require()
private val _state = MutableStateFlow(
value = NFTCollectionsStateUM(
onBackClick = ::navigateBack,
content = NFTCollectionsUM.Loading(::onReceiveClick),
onBackClick = params.onBackClick,
content = NFTCollectionsUM.Loading(params.onBackClick),
),
)
private val params: NFTCollectionsComponent.Params = paramsContainer.require()
val state: StateFlow<NFTCollectionsStateUM> get() = _state
init {
subscribeToNFTCollections()
@ -60,11 +56,15 @@ internal class NFTCollectionsModel @Inject constructor(
UpdateDataStateTransformer(
nftCollections = nftCollections,
searchQuery = query,
onReceiveClick = ::onReceiveClick,
onReceiveClick = {
params.onReceiveClick()
},
onRetryClick = ::onRetryClick,
onExpandCollectionClick = ::onExpandCollectionClick,
onRetryAssetsClick = ::onRetryAssetsClick,
onAssetClick = ::onAssetClick,
onAssetClick = { asset, collectionName ->
params.onAssetClick(asset, collectionName)
},
initialSearchBarFactory = ::getInitialSearchBar,
).transform(it)
}
@ -115,27 +115,6 @@ internal class NFTCollectionsModel @Inject constructor(
// TODO refresh all
}
private fun onAssetClick(asset: NFTAsset) {
router.push(
AppRoute.NFTDetails(
userWalletId = params.userWalletId,
nftAsset = asset,
),
)
}
private fun onReceiveClick() {
router.push(
AppRoute.NFTReceive(
userWalletId = params.userWalletId,
),
)
}
private fun navigateBack() {
router.pop()
}
private fun loadCollectionAssets(collection: NFTCollection) {
modelScope.launch {
fetchNFTCollectionAssetsUseCase(

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,172 @@
package com.tangem.features.nft.common
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.nft.collections.NFTCollectionsComponent
import com.tangem.features.nft.common.ui.NFTContent
import com.tangem.features.nft.component.NFTComponent
import com.tangem.features.nft.details.NFTDetailsComponent
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
internal class DefaultNFTComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTComponent.Params,
) : NFTComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<NFTRoute>()
private val innerRouter = InnerRouter<NFTRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId)
private val currentRoute = MutableStateFlow(initialRoute)
private val childStack = childStack(
key = "sendInnerStack",
source = stackNavigation,
serializer = null,
initialConfiguration = initialRoute,
handleBackButton = true,
childFactory = { configuration, factoryContext ->
createChild(
configuration,
childByContext(
componentContext = factoryContext,
router = innerRouter,
),
)
},
)
init {
childStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
componentScope.launch {
currentRoute.emit(stack.active.configuration)
}
}
}
@Composable
override fun Content(modifier: Modifier) {
val stackState by childStack.subscribeAsState()
BackHandler(onBack = ::onChildBack)
NFTContent(
stackState = stackState,
)
}
private fun createChild(route: NFTRoute, factoryContext: AppComponentContext) = when (route) {
is NFTRoute.Collections -> getCollectionsComponent(factoryContext, route)
is NFTRoute.Receive -> getReceiveComponent(factoryContext, route)
is NFTRoute.Details -> getDetailsComponent(factoryContext, route)
is NFTRoute.AssetTraits -> getAssetTraitsComponent(factoryContext, route)
}
private fun getCollectionsComponent(
factoryContext: AppComponentContext,
route: NFTRoute.Collections,
): ComposableContentComponent = NFTCollectionsComponent(
context = factoryContext,
params = NFTCollectionsComponent.Params(
userWalletId = route.userWalletId,
onBackClick = ::onChildBack,
onReceiveClick = {
innerRouter.push(
NFTRoute.Receive(
userWalletId = route.userWalletId,
),
)
},
onAssetClick = { asset, collectionName ->
innerRouter.push(
NFTRoute.Details(
userWalletId = route.userWalletId,
nftAsset = asset,
collectionName = collectionName,
),
)
},
),
)
private fun getReceiveComponent(
factoryContext: AppComponentContext,
route: NFTRoute.Receive,
): ComposableContentComponent = NFTReceiveComponent(
context = factoryContext,
params = NFTReceiveComponent.Params(
userWalletId = route.userWalletId,
onBackClick = ::onChildBack,
),
)
private fun getDetailsComponent(
factoryContext: AppComponentContext,
route: NFTRoute.Details,
): ComposableContentComponent = NFTDetailsComponent(
context = factoryContext,
params = NFTDetailsComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.collectionName,
onBackClick = ::onChildBack,
onAllTraitsClick = {
innerRouter.push(
NFTRoute.AssetTraits(
nftAsset = route.nftAsset,
),
)
},
),
)
private fun getAssetTraitsComponent(
factoryContext: AppComponentContext,
route: NFTRoute.AssetTraits,
): ComposableContentComponent = NFTAssetTraitsComponent(
context = factoryContext,
params = NFTAssetTraitsComponent.Params(
nftAsset = route.nftAsset,
onBackClick = ::onChildBack,
),
)
private fun onChildBack() {
val isEmptyStack = childStack.value.backStack.isEmpty()
if (isEmptyStack) {
router.pop()
} else {
stackNavigation.pop()
}
}
@AssistedFactory
interface Factory : NFTComponent.Factory {
override fun create(context: AppComponentContext, params: NFTComponent.Params): DefaultNFTComponent
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.nft.common
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
internal sealed class NFTRoute : Route {
@Serializable
data class Collections(
val userWalletId: UserWalletId,
) : NFTRoute()
@Serializable
data class Receive(
val userWalletId: UserWalletId,
) : NFTRoute()
@Serializable
data class Details(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val collectionName: String,
) : NFTRoute()
@Serializable
data class AssetTraits(
val nftAsset: NFTAsset,
) : NFTRoute()
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.nft.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
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.core.ui.res.TangemTheme
import com.tangem.features.nft.common.NFTRoute
@Composable
internal fun NFTContent(stackState: ChildStack<NFTRoute, ComposableContentComponent>) {
Column(
modifier = Modifier.Companion
.background(color = TangemTheme.colors.background.tertiary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
horizontalAlignment = Alignment.Companion.CenterHorizontally,
) {
Children(
stack = stackState,
animation = stackAnimation(slide()),
modifier = Modifier.weight(1f),
) {
it.instance.Content(Modifier.weight(1f))
}
}
}

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

@ -6,17 +6,18 @@ 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.NFTDetailsComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.details.model.NFTDetailsModel
import com.tangem.features.nft.details.ui.NFTDetails
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTDetailsComponent @AssistedInject constructor(
internal class NFTDetailsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTDetailsComponent.Params,
) : NFTDetailsComponent, AppComponentContext by context {
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: NFTDetailsModel = getOrCreateModel(params)
@ -27,11 +28,11 @@ internal class DefaultNFTDetailsComponent @AssistedInject constructor(
NFTDetails(state, modifier)
}
@AssistedFactory
interface Factory : NFTDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTDetailsComponent.Params,
): DefaultNFTDetailsComponent
}
data class Params(
val userWalletId: UserWalletId,
val nftAsset: NFTAsset,
val nftCollectionName: String,
val onBackClick: () -> Unit,
val onAllTraitsClick: () -> Unit,
)
}

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,15 +1,17 @@
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
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.details.NFTDetailsComponent
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
@ -33,9 +35,9 @@ internal class NFTDetailsModel @Inject constructor(
traits = persistentListOf(),
baseInfoItems = persistentListOf(),
),
onBackClick = ::navigateBack,
onBackClick = params.onBackClick,
onReadMoreClick = ::onReadMoreClick,
onSeeAllClick = ::onSeeAllClick,
onSeeAllTraitsClick = params.onAllTraitsClick,
onExploreClick = ::onExploreClick,
onSendClick = ::onSendClick,
bottomSheetConfig = null,
@ -46,19 +48,17 @@ internal class NFTDetailsModel @Inject constructor(
// TODO implement
}
private fun onSeeAllClick() {
// TODO implement
}
private fun onExploreClick() {
// TODO implement
}
private fun onSendClick() {
// TODO implement
}
private fun navigateBack() {
router.pop()
router.push(
AppRoute.NFTSend(
userWalletId = params.userWalletId,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
),
)
}
}

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,16 @@
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.collections.DefaultNFTCollectionsComponent
import com.tangem.features.nft.DefaultNFTFeatureToggles
import com.tangem.features.nft.NFTFeatureToggles
import com.tangem.features.nft.collections.model.NFTCollectionsModel
import com.tangem.features.nft.component.NFTCollectionsComponent
import com.tangem.features.nft.component.NFTDetailsComponent
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.details.DefaultNFTDetailsComponent
import com.tangem.features.nft.common.DefaultNFTComponent
import com.tangem.features.nft.component.*
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.model.NFTAssetTraitsModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@ -36,30 +36,31 @@ internal object NFTFeatureModule {
internal interface NFTFeatureModuleBinds {
@Binds
@Singleton
fun bindNFTCollectionsComponentFactory(
impl: DefaultNFTCollectionsComponent.Factory,
): NFTCollectionsComponent.Factory
fun bindNFTComponentFactory(impl: DefaultNFTComponent.Factory): NFTComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTCollectionsModel::class)
fun bindNFTCollectionsModel(model: NFTCollectionsModel): Model
@Binds
@Singleton
fun bindNFTReceiveComponentFactory(impl: DefaultNFTReceiveComponent.Factory): NFTReceiveComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTReceiveModel::class)
fun bindNFTReceiveModel(model: NFTReceiveModel): Model
@Binds
@Singleton
fun bindNFTDetailsComponentFactory(impl: DefaultNFTDetailsComponent.Factory): NFTDetailsComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTDetailsModel::class)
fun bindNFTDetailsModel(model: NFTDetailsModel): Model
@Binds
@Singleton
fun bindNFTDetailsBlockComponentFactory(
impl: DefaultNFTDetailsBlockComponent.Factory,
): NFTDetailsBlockComponent.Factory
@Binds
@IntoMap
@ClassKey(NFTAssetTraitsModel::class)
fun bindNFTTraitsModel(model: NFTAssetTraitsModel): Model
}

View file

@ -6,17 +6,17 @@ 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.NFTReceiveComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.nft.receive.model.NFTReceiveModel
import com.tangem.features.nft.receive.ui.NFTReceive
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNFTReceiveComponent @AssistedInject constructor(
internal class NFTReceiveComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: NFTReceiveComponent.Params,
) : NFTReceiveComponent, AppComponentContext by context {
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: NFTReceiveModel = getOrCreateModel(params)
@ -27,11 +27,8 @@ internal class DefaultNFTReceiveComponent @AssistedInject constructor(
NFTReceive(state, modifier)
}
@AssistedFactory
interface Factory : NFTReceiveComponent.Factory {
override fun create(
context: AppComponentContext,
params: NFTReceiveComponent.Params,
): DefaultNFTReceiveComponent
}
data class Params(
val userWalletId: UserWalletId,
val onBackClick: () -> Unit,
)
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.nft.receive.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.core.navigation.share.ShareManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.fields.InputManager
@ -14,8 +13,8 @@ import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase
import com.tangem.domain.nft.GetNFTNetworkStatusUseCase
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.features.nft.component.NFTReceiveComponent
import com.tangem.features.nft.impl.R
import com.tangem.features.nft.receive.NFTReceiveComponent
import com.tangem.features.nft.receive.entity.NFTReceiveUM
import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheetTransformer
import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer
@ -31,7 +30,6 @@ import javax.inject.Inject
@ModelScoped
internal class NFTReceiveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val searchManager: InputManager,
private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase,
private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase,
@ -41,18 +39,18 @@ internal class NFTReceiveModel @Inject constructor(
paramsContainer: ParamsContainer,
) : Model() {
val state: StateFlow<NFTReceiveUM> get() = _state
private val params: NFTReceiveComponent.Params = paramsContainer.require()
private val _state = MutableStateFlow(
value = NFTReceiveUM(
onBackClick = ::navigateBack,
onBackClick = params.onBackClick,
search = getInitialSearchBar(),
networks = NFTReceiveUM.Networks.Content(persistentListOf()),
bottomSheetConfig = null,
),
)
private val params: NFTReceiveComponent.Params = paramsContainer.require()
val state: StateFlow<NFTReceiveUM> get() = _state
init {
subscribeToNFTAvailableNetworks()
@ -140,8 +138,4 @@ internal class NFTReceiveModel @Inject constructor(
private fun onShareClick(text: String) {
shareManager.shareText(text = text)
}
private fun navigateBack() {
router.pop()
}
}

View file

@ -0,0 +1,34 @@
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.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.features.nft.traits.model.NFTAssetTraitsModel
import com.tangem.features.nft.traits.ui.NFTAssetTraits
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class NFTAssetTraitsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: NFTAssetTraitsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
NFTAssetTraits(state)
}
data class Params(
val nftAsset: NFTAsset,
val onBackClick: () -> Unit,
)
}

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,42 @@
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.domain.nft.models.NFTAsset
import com.tangem.features.nft.traits.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,
paramsContainer: ParamsContainer,
) : Model() {
private val params: NFTAssetTraitsComponent.Params = paramsContainer.require()
private val _state = MutableStateFlow(
value = NFTAssetTraitsUM(
traits = params.nftAsset.transform(),
onBackClick = { params.onBackClick() },
),
)
val state: StateFlow<NFTAssetTraitsUM> get() = _state
private fun NFTAsset.transform(): ImmutableList<NFTAssetTraitUM> = this
.traits
.mapIndexed { index, trait ->
NFTAssetTraitUM(
id = index.toString(),
name = trait.name,
value = trait.value,
)
}.toPersistentList()
}

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

@ -7,14 +7,15 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.features.onboarding.v2.impl.R
@Composable
internal fun WalletCard(url: String?, modifier: Modifier = Modifier) {
internal fun WalletCard(artwork: ArtworkUM?, modifier: Modifier = Modifier) {
AsyncImage(
modifier = modifier,
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.data(artwork?.verifiedArtwork ?: artwork?.defaultUrl)
.crossfade(true)
.build(),
placeholder = painterResource(R.drawable.card_placeholder_black),

View file

@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.replaceAll
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.models.scan.ProductType
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -12,7 +13,6 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
@ -29,6 +29,7 @@ import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEve
import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
@ -150,10 +151,16 @@ internal class OnboardingEntryModel @Inject constructor(
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
modelScope.launch {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened)
}
stackNavigation.replaceAll(
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)),
)
} else {
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened)
}
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
@ -163,6 +170,9 @@ internal class OnboardingEntryModel @Inject constructor(
}
}
} else {
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened)
}
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
@ -177,6 +187,9 @@ internal class OnboardingEntryModel @Inject constructor(
) : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On))
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened)
}
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
@ -187,6 +200,9 @@ internal class OnboardingEntryModel @Inject constructor(
override fun onDenied() {
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off))
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened)
}
stackNavigation.replaceAll(
OnboardingRoute.Done(
mode = doneMode,
@ -232,6 +248,12 @@ internal class OnboardingEntryModel @Inject constructor(
}
}
private inline fun doIfVisa(function: () -> Unit) {
if (params.scanResponse.productType == ProductType.Visa) {
function()
}
}
inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks {
override fun onDone() {
navigateToFinalScreenFlow()

View file

@ -6,6 +6,7 @@ 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.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.onboarding.repository.OnboardingRepository
@ -16,6 +17,7 @@ import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -78,18 +80,26 @@ internal class OnboardingMultiWalletModel @Inject constructor(
modelScope.launch {
backups.collectLatest { backup ->
when {
_uiState.value.artwork2Url == null && backup.card2 != null -> {
_uiState.value.artwork2 == null && backup.card2 != null -> {
val artwork =
getCardImageUseCase.invoke(backup.card2.cardId, backup.card2.cardPublicKey)
getCardImageUseCase.invoke(
cardId = backup.card2.cardId,
cardPublicKey = backup.card2.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(artwork2Url = artwork)
it.copy(artwork2 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
_uiState.value.artwork3Url == null && backup.card3 != null -> {
_uiState.value.artwork3 == null && backup.card3 != null -> {
val artwork =
getCardImageUseCase.invoke(backup.card3.cardId, backup.card3.cardPublicKey)
getCardImageUseCase.invoke(
cardId = backup.card3.cardId,
cardPublicKey = backup.card3.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(artwork3Url = artwork)
it.copy(artwork3 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}
@ -156,12 +166,14 @@ internal class OnboardingMultiWalletModel @Inject constructor(
private fun loadCardArtwork() {
modelScope.launch {
val artwork =
getCardImageUseCase.invoke(params.scanResponse.card.cardId, params.scanResponse.card.cardPublicKey)
getCardImageUseCase.invoke(
cardId = params.scanResponse.card.cardId,
cardPublicKey = params.scanResponse.card.cardPublicKey,
size = ArtworkSize.LARGE,
)
if (artwork != getCardImageUseCase.getDefaultFallbackUrl()) {
_uiState.update {
it.copy(artwork1Url = artwork)
}
_uiState.update {
it.copy(artwork1 = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}

View file

@ -25,9 +25,9 @@ internal fun OnboardingMultiWallet(
) {
if (isSeedPhraseState.not()) {
WalletArtworks(
url1 = state.artwork1Url,
url2 = state.artwork2Url,
url3 = state.artwork3Url,
artwork1 = state.artwork1,
artwork2 = state.artwork2,
artwork3 = state.artwork3,
modifier = Modifier
.padding(horizontal = 34.dp)
.padding(top = 20.dp)
@ -53,7 +53,7 @@ internal fun OnboardingMultiWallet(
private fun Preview() {
TangemThemePreview {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artwork1Url = null),
state = OnboardingMultiWalletUM(artwork1 = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = false,
childContent = { md ->
@ -73,7 +73,7 @@ private fun Preview() {
private fun Preview2() {
TangemThemePreview {
OnboardingMultiWallet(
state = OnboardingMultiWalletUM(artwork1Url = null),
state = OnboardingMultiWalletUM(artwork1 = null),
artworksState = WalletArtworksState.Folded,
isSeedPhraseState = true,
childContent = { md ->

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.common.ui.WalletCard
@ -57,9 +58,9 @@ sealed class WalletArtworksState {
@Composable
fun WalletArtworks(
url1: String?,
url2: String?,
url3: String?,
artwork1: ArtworkUM?,
artwork2: ArtworkUM?,
artwork3: ArtworkUM?,
state: WalletArtworksState,
modifier: Modifier = Modifier,
) {
@ -120,9 +121,9 @@ fun WalletArtworks(
}
AnimatedWalletCards(
url1 = url1,
url2 = url2,
url3 = url3,
artwork1 = artwork1,
artwork2 = artwork2,
artwork3 = artwork3,
modifier = Modifier
.widthIn(max = 450.dp)
.matchParentSize(),
@ -152,9 +153,9 @@ private data class WalletCardTransitionState(
@Suppress("LongParameterList")
@Composable
private fun AnimatedWalletCards(
url1: String?,
url2: String?,
url3: String?,
artwork1: ArtworkUM?,
artwork2: ArtworkUM?,
artwork3: ArtworkUM?,
transition1: Transition<WalletCardTransitionState>,
transition2: Transition<WalletCardTransitionState>,
transition3: Transition<WalletCardTransitionState>,
@ -199,7 +200,7 @@ private fun AnimatedWalletCards(
scaleY = scaleY3
rotationZ = rotation3
},
url = url3,
artwork = artwork3,
)
WalletCard(
@ -217,7 +218,7 @@ private fun AnimatedWalletCards(
scaleX = scaleX2
scaleY = scaleY2
},
url = url2,
artwork = artwork2,
)
WalletCard(
@ -234,7 +235,7 @@ private fun AnimatedWalletCards(
translationY = translationY1
rotationZ = rotation1
},
url = url1,
artwork = artwork1,
)
}
}
@ -447,9 +448,9 @@ private fun Preview() {
var state: WalletArtworksState by remember { mutableStateOf(WalletArtworksState.Folded) }
WalletArtworks(
url1 = null,
url2 = null,
url3 = null,
artwork1 = null,
artwork2 = null,
artwork3 = null,
state = state,
modifier = Modifier.fillMaxWidth(),
)

View file

@ -1,7 +1,9 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
internal data class OnboardingMultiWalletUM(
val artwork1Url: String? = null,
val artwork2Url: String? = null,
val artwork3Url: String? = null,
val artwork1: ArtworkUM? = null,
val artwork2: ArtworkUM? = null,
val artwork3: ArtworkUM? = null,
)

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
@ -93,9 +94,11 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
private fun observeArtwork() {
modelScope.launch {
params.childParams.commonState.collect {
params.childParams.commonState.collect { state ->
_uiState.value = _uiState.value.copy(
artworkUrl = it.cardArtworkUrl,
artwork = state.cardArtwork?.let {
ArtworkUM(it.verifiedArtwork, it.defaultUrl)
},
)
}
}

View file

@ -32,9 +32,9 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod
verticalArrangement = Arrangement.Bottom,
) {
WalletArtworks(
url1 = state.artworkUrl,
url2 = null,
url3 = null,
artwork1 = state.artwork,
artwork2 = null,
artwork3 = null,
modifier = Modifier
.padding(top = 64.dp)
.padding(horizontal = 24.dp)
@ -86,7 +86,7 @@ private fun OnboardingNoteCreatePreview() {
TangemThemePreview {
OnboardingNoteCreateWallet(
state = OnboardingNoteCreateWalletUM(
artworkUrl = null,
artwork = null,
createWalletInProgress = false,
onCreateClick = {},
),

View file

@ -1,7 +1,9 @@
package com.tangem.features.onboarding.v2.note.impl.child.create.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
data class OnboardingNoteCreateWalletUM(
val artworkUrl: String? = null,
val artwork: ArtworkUM? = null,
val createWalletInProgress: Boolean = false,
val onCreateClick: () -> Unit = {},
)

View file

@ -129,7 +129,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
modelScope.launch {
params.childParams.commonState.collect {
_uiState.value = _uiState.value.copy(
cardArtworkUrl = it.cardArtworkUrl,
cardArtwork = it.cardArtwork,
)
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalTangemShimmer
import com.tangem.core.ui.res.TangemTheme
@ -21,11 +22,12 @@ import com.tangem.features.onboarding.v2.common.ui.RefreshButton
import com.tangem.features.onboarding.v2.common.ui.WalletCard
import com.tangem.features.onboarding.v2.impl.R
import com.valentinilk.shimmer.shimmer
import kotlinx.collections.immutable.ImmutableList
@Composable
fun OnboardingNoteTopUpHeader(
balance: String,
cardArtworkUrl: String?,
cardArtwork: ArtworkUM?,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
modifier: Modifier = Modifier,
@ -77,7 +79,7 @@ fun OnboardingNoteTopUpHeader(
}
WalletCard(
modifier = Modifier.width(120.dp).align(Alignment.TopCenter),
url = cardArtworkUrl,
artwork = cardArtwork,
)
RefreshButton(
modifier = Modifier.align(Alignment.BottomCenter),
@ -93,7 +95,7 @@ private fun OnboardinNoteTopUpHeaderPreview() {
TangemThemePreview {
OnboardingNoteTopUpHeader(
balance = "0.00000001 BTC",
cardArtworkUrl = "",
cardArtwork = ArtworkUM(null as ImmutableList<Byte>?, ""),
onRefreshBalanceClick = {},
isRefreshing = false,
)

View file

@ -34,7 +34,7 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif
) {
OnboardingNoteTopUpHeader(
balance = state.balance,
cardArtworkUrl = state.cardArtworkUrl,
cardArtwork = state.cardArtwork,
onRefreshBalanceClick = state.onRefreshBalanceClick,
isRefreshing = state.isRefreshing,
modifier = Modifier

View file

@ -1,9 +1,10 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
data class OnboardingNoteTopUpUM(
val cardArtworkUrl: String? = null,
val cardArtwork: ArtworkUM? = null,
val availableForBuy: Boolean = false,
val availableForBuyLoading: Boolean = true,
val balance: String = "",

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.note.impl.model
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
@ -7,6 +8,6 @@ import com.tangem.domain.wallets.models.UserWallet
internal data class OnboardingNoteCommonState(
val scanResponse: ScanResponse,
val userWallet: UserWallet? = null,
val cardArtworkUrl: String? = null,
val cardArtwork: ArtworkUM? = null,
val cryptoCurrencyStatus: CryptoCurrencyStatus? = null,
)

View file

@ -6,6 +6,7 @@ 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.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog
@ -13,6 +14,7 @@ import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
import com.tangem.features.onboarding.v2.note.impl.route.stepNum
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -50,7 +52,7 @@ internal class OnboardingNoteModel @Inject constructor(
val commonUiState: MutableStateFlow<OnboardingNoteCommonState> = _uiState
init {
loadArtworkUrl()
loadArtwork()
}
fun updateStepForNewRoute(route: OnboardingNoteRoute) {
@ -85,15 +87,16 @@ internal class OnboardingNoteModel @Inject constructor(
}
}
private fun loadArtworkUrl() {
private fun loadArtwork() {
modelScope.launch {
val cardInfo = params.scanResponse.card
val url = getCardImageUseCase(
val artwork = getCardImageUseCase.invoke(
cardId = cardInfo.cardId,
cardPublicKey = cardInfo.cardPublicKey,
size = ArtworkSize.LARGE,
)
_uiState.update {
it.copy(cardArtworkUrl = url)
it.copy(cardArtwork = ArtworkUM(artwork.verifiedArtwork, artwork.defaultUrl))
}
}
}

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

@ -23,6 +23,7 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -212,7 +213,7 @@ private fun AnimatedTwinCards(
scaleX = scaleX2
scaleY = scaleY2
},
url = Artwork.TWIN_CARD_2_URL,
artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_2_URL),
)
WalletCard(
@ -229,7 +230,7 @@ private fun AnimatedTwinCards(
translationY = translationY1
rotationZ = rotation1
},
url = Artwork.TWIN_CARD_1_URL,
artwork = ArtworkUM(defaultUrl = Artwork.TWIN_CARD_1_URL),
)
}
}

View file

@ -167,7 +167,9 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
params = OnboardingVisaOtherWalletComponent.Params(
childParams = childParams,
onDone = {
model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it))
model.stackNavigation.pushNew(
OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false),
)
},
),
)
@ -176,6 +178,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor(
config = OnboardingVisaPinCodeComponent.Config(
scanResponse = model.currentScanResponse.value,
activationOrderInfo = route.activationOrderInfo,
wasValidationError = route.pinCodeValidationError,
),
params = OnboardingVisaPinCodeComponent.Params(
childParams = childParams,

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -19,6 +20,9 @@ import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
@ -30,6 +34,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@Stable
@ModelScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor(
@ -40,6 +45,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
private val uiMessageSender: UiMessageSender,
private val analyticsEventsHandler: AnalyticsEventHandler,
) : Model() {
private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require()
@ -59,6 +65,10 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
val onBack = MutableSharedFlow<Unit>()
val onDone = MutableSharedFlow<ActivationReadyEvent>()
init {
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.SettingAccessCodeStarted)
}
fun onBack() {
if (uiState.value.buttonLoading) return
@ -82,6 +92,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
_uiState.update {
it.copy(
accessCodeFirst = textFieldValue,
accessCodeSecond = TextFieldValue(),
atLeastMinCharsError = false,
)
}
@ -99,11 +110,14 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
private fun onContinue() {
when (uiState.value.step) {
OnboardingVisaAccessCodeUM.Step.Enter -> {
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeEntered)
if (checkAccessCodeMinChars().not()) return
_uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) }
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.AccessCodeReenterScreen)
}
OnboardingVisaAccessCodeUM.Step.ReEnter -> {
if (checkAccessCodesMatch().not()) return
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.OnboardingVisa)
startActivationProcess(accessCode = uiState.value.accessCodeFirst.text)
}
}
@ -160,6 +174,9 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
)
return@launch
}
is CompletionResult.Success -> result.data

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -15,6 +16,9 @@ import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.approve.OnboardingVisaApproveComponent
import com.tangem.features.onboarding.v2.visa.impl.child.approve.ui.state.OnboardingVisaApproveUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
@ -32,6 +36,7 @@ internal class OnboardingVisaApproveModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<OnboardingVisaApproveComponent.Config>()
@ -47,6 +52,10 @@ internal class OnboardingVisaApproveModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
init {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.WalletPrepare)
}
private fun getInitialState(): OnboardingVisaApproveUM {
return OnboardingVisaApproveUM(
onApproveClick = ::onApproveClick,
@ -56,6 +65,8 @@ internal class OnboardingVisaApproveModel @Inject constructor(
private fun onApproveClick() {
loading(true)
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove)
modelScope.launch {
val dataToSign = runCatching {
visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request)
@ -77,6 +88,9 @@ internal class OnboardingVisaApproveModel @Inject constructor(
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
)
return@launch
}
is CompletionResult.Success -> result.data

View file

@ -1,6 +1,7 @@
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.resourceReference
@ -8,6 +9,7 @@ import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM
import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.SelectableChainRowUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableSharedFlow
@ -21,6 +23,7 @@ import javax.inject.Inject
@ModelScoped
internal class OnboardingVisaChooseWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
@ -53,7 +56,18 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor(
private fun onContinueClicked() {
modelScope.launch {
val selectedOption = _uiState.value.selectedOption ?: return@launch
val value = getAnalyticsValue(selectedOption.event)
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ChooseWalletScreen(value))
onEvent.emit(selectedOption.event)
}
}
private fun getAnalyticsValue(event: Event) = when (event) {
Event.TangemWallet -> {
"Tangem"
}
Event.OtherWallet -> {
"Other"
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model
import androidx.compose.runtime.Stable
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -20,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
@ -41,6 +43,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<Config>()
@ -53,6 +56,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
val onDone = MutableSharedFlow<Params.DoneEvent>()
init {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen)
modelScope.launch {
while (true) {
val result = runCatching {
@ -71,9 +75,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
VisaActivationRemoteState.WaitingForActivationFinishing,
-> {
}
is VisaActivationRemoteState.WaitingPinCode -> {
navigateToPinCode(result.activationOrderInfo)
return@launch
is VisaActivationRemoteState.AwaitingPinCode -> {
navigateToPinCodeIfNeeded(result) {
return@launch
}
}
VisaActivationRemoteState.Activated -> {
finishActivation()
@ -86,8 +91,37 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
}
}
private suspend fun navigateToPinCode(activationOrderInfo: VisaActivationOrderInfo) {
onDone.emit(Params.DoneEvent.NavigateTo(OnboardingVisaRoute.PinCode(activationOrderInfo)))
private suspend inline fun navigateToPinCodeIfNeeded(
remoteState: VisaActivationRemoteState.AwaitingPinCode,
returnBlock: () -> Unit,
) {
when (remoteState.status) {
VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> {
onDone.emit(
Params.DoneEvent.NavigateTo(
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = false,
),
),
)
returnBlock()
}
VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> {
onDone.emit(
Params.DoneEvent.NavigateTo(
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = true,
),
),
)
returnBlock()
}
VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> {
/** waiting for new state */
}
}
}
private suspend fun finishActivation() {

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.model
import androidx.compose.runtime.Stable
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -13,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@ -29,6 +31,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val config = paramsContainer.require<OnboardingVisaOtherWalletComponent.Config>()
@ -44,13 +47,14 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
val onDone = MutableSharedFlow<VisaActivationOrderInfo>()
init {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened)
modelScope.launch {
while (true) {
val result = runCatching {
visaActivationRepository.getActivationRemoteStateLongPoll()
}.getOrNull()
if (result is VisaActivationRemoteState.WaitingPinCode) {
if (result is VisaActivationRemoteState.AwaitingPinCode) {
onDone.emit(result.activationOrderInfo)
break
}
@ -68,10 +72,12 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
}
private fun onShareClicked() {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonShareLink)
shareManager.shareText("https://tangem.com/") // TODO
}
private fun onOpenInBrowserClicked() {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonBrowser)
urlOpener.openUrl("https://tangem.com/") // TODO
}
}

View file

@ -48,6 +48,7 @@ internal class OnboardingVisaPinCodeComponent(
data class Config(
val scanResponse: ScanResponse,
val activationOrderInfo: VisaActivationOrderInfo,
val wasValidationError: Boolean,
)
data class Params(

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model
import androidx.compose.runtime.Stable
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -17,6 +18,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -37,6 +39,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
private val authTokenStorage: VisaAuthTokenStorage,
private val otpStorage: VisaAuthTokenStorage,
private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<OnboardingVisaPinCodeComponent.Config>()
@ -49,11 +52,20 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<Unit>()
init {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinCodeScreenOpened)
}
private fun getInitialState(): OnboardingVisaPinCodeUM {
return OnboardingVisaPinCodeUM(
submitButtonEnabled = false,
onPinCodeChange = ::onPinCodeChange,
onSubmitClick = ::onSubmitClick,
error = if (params.wasValidationError) {
resourceReference(R.string.visa_onboarding_pin_not_accepted)
} else {
null
},
)
}
@ -66,6 +78,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
pinCode = pin,
submitButtonEnabled = PinCodeValidation.validate(pin),
error = if (isError) {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ErrorPinValidation)
resourceReference(R.string.visa_onboarding_pin_validation_error_message)
} else {
null
@ -76,6 +89,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
}
private fun onSubmitClick() {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.PinEntered)
val pinCode = _uiState.value.pinCode
if (PinCodeValidation.validate(pinCode).not()) return

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -14,6 +15,9 @@ import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.Config
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.OnboardingVisaWelcomeComponent.DoneEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.ONBOARDING_SOURCE
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state.OnboardingVisaWelcomeUM
import com.tangem.features.onboarding.v2.visa.impl.common.ActivationReadyEvent
import com.tangem.sdk.api.TangemSdkManager
@ -33,6 +37,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val uiMessageSender: UiMessageSender,
private val analyticsEventsHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<Config>()
@ -42,6 +47,10 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val onDone = MutableSharedFlow<DoneEvent>()
init {
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ActivationScreenOpened)
}
private fun getInitialState(): OnboardingVisaWelcomeUM {
return OnboardingVisaWelcomeUM(
mode = if (params is Config.WelcomeBack) {
@ -54,6 +63,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
}
private fun onContinueClick() {
analyticsEventsHandler.send(OnboardingVisaAnalyticsEvent.ButtonActivate)
if (params !is Config.WelcomeBack) {
modelScope.launch { onDone.emit(DoneEvent.WelcomeDone) }
return
@ -86,6 +96,9 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(
VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE),
)
return@launch
}
is CompletionResult.Success -> result.data

View file

@ -0,0 +1,85 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal sealed class OnboardingVisaAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Onboarding / Visa", event, params) {
data object ActivationScreenOpened : OnboardingVisaAnalyticsEvent(
event = "Activation Screen Opened",
)
data object ButtonActivate : OnboardingVisaAnalyticsEvent(
event = "Button - Activate",
)
data object SettingAccessCodeStarted : OnboardingVisaAnalyticsEvent(
event = "Setting Access Code Started",
)
data object AccessCodeEntered : OnboardingVisaAnalyticsEvent(
event = "Access Code Entered",
)
data object AccessCodeReenterScreen : OnboardingVisaAnalyticsEvent(
event = "Access Code Re-enter Screen",
)
data object OnboardingVisa : OnboardingVisaAnalyticsEvent(
event = "Onboarding / Visa",
)
data class ChooseWalletScreen(
val type: String,
) : OnboardingVisaAnalyticsEvent(
event = "Choose Wallet Screen",
params = mapOf("Type" to type),
)
data object WalletPrepare : OnboardingVisaAnalyticsEvent(
event = "Wallet Prepare",
)
data object ButtonApprove : OnboardingVisaAnalyticsEvent(
event = "Button - Approve",
)
data object GoToWebsiteOpened : OnboardingVisaAnalyticsEvent(
event = "Go To Website Opened",
)
data object ButtonBrowser : OnboardingVisaAnalyticsEvent(
event = "Button - Browser",
)
data object ButtonShareLink : OnboardingVisaAnalyticsEvent(
event = "Button - Share Link",
)
data object ActivationInProgressScreen : OnboardingVisaAnalyticsEvent(
event = "Activation In Progress Screen",
)
data object PinCodeScreenOpened : OnboardingVisaAnalyticsEvent(
event = "PIN Code Screen Opened",
)
data object PinEntered : OnboardingVisaAnalyticsEvent(
event = "PIN Entered",
params = mapOf("Type" to "Visa"),
)
data object BiometricScreenOpened : OnboardingVisaAnalyticsEvent(
event = "Biometric Screen Opened",
)
data object SuccessScreenOpened : OnboardingVisaAnalyticsEvent(
event = "Success Screen Opened",
)
data object ErrorPinValidation : OnboardingVisaAnalyticsEvent(
event = "Error - Pin Validation",
)
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
internal const val ONBOARDING_SOURCE = "Onboarding"
internal const val MAIN_SOURCE = "Main"
internal sealed class VisaAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Visa", event, params) {
data class Errors(
val errorCode: String,
val source: String,
) : VisaAnalyticsEvent(
event = "Errors",
params = mapOf(
"Error Code" to errorCode,
"Source" to source,
),
)
}

View file

@ -159,8 +159,11 @@ internal class OnboardingVisaModel @Inject constructor(
VisaActivationRemoteState.WaitingForActivationFinishing -> {
OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode)
}
is VisaActivationRemoteState.WaitingPinCode -> {
OnboardingVisaRoute.PinCode(activationOrderInfo = remoteState.activationOrderInfo)
is VisaActivationRemoteState.AwaitingPinCode -> {
OnboardingVisaRoute.PinCode(
activationOrderInfo = remoteState.activationOrderInfo,
pinCodeValidationError = false,
)
}
VisaActivationRemoteState.Activated,
VisaActivationRemoteState.BlockedForActivation,

View file

@ -48,5 +48,8 @@ internal sealed class OnboardingVisaRoute {
}
@Serializable
data class PinCode(val activationOrderInfo: VisaActivationOrderInfo) : OnboardingVisaRoute()
data class PinCode(
val activationOrderInfo: VisaActivationOrderInfo,
val pinCodeValidationError: Boolean,
) : OnboardingVisaRoute()
}

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

@ -15,6 +15,7 @@ dependencies {
/** Api */
implementation(projects.features.sendV2.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.nft.api)
/** Libs */
implementation(projects.libs.crypto)
@ -54,6 +55,7 @@ dependencies {
implementation(projects.domain.txhistory)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.nft.models)
/** Compose libraries */
implementation(deps.compose.foundation)

View file

@ -1,35 +1,34 @@
package com.tangem.features.send.v2.send
package com.tangem.features.send.v2.common
import com.tangem.core.decompose.navigation.Route
import kotlinx.serialization.Serializable
@Serializable
internal sealed class SendRoute : Route {
internal sealed class CommonSendRoute : Route {
abstract val isEditMode: Boolean
@Serializable
data object Empty : SendRoute() {
data object Empty : CommonSendRoute() {
override val isEditMode = false
}
@Serializable
data object Confirm : SendRoute() {
data object Confirm : CommonSendRoute() {
override val isEditMode: Boolean = false
}
@Serializable
data class Destination(
override val isEditMode: Boolean,
) : SendRoute()
) : CommonSendRoute()
@Serializable
data class Amount(
override val isEditMode: Boolean,
) : SendRoute()
) : CommonSendRoute()
@Serializable
data class Fee(
override val isEditMode: Boolean = true,
) : SendRoute()
data object Fee : CommonSendRoute() {
override val isEditMode: Boolean = true
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.send.v2.common
sealed class PredefinedValues {
data object Empty : PredefinedValues()
sealed class Content : PredefinedValues() {
abstract val amount: String
abstract val address: String
abstract val memo: String?
data class Deeplink(
override val amount: String,
override val address: String,
override val memo: String?,
val transactionId: String,
) : Content()
data class QrCode(
override val amount: String,
override val address: String,
override val memo: String?,
) : Content()
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.features.send.v2.common
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.coroutines.DelayedWork
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
@Suppress("LongParameterList")
internal class SendBalanceUpdater @AssistedInject constructor(
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val updateDelayedNetworkStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
@DelayedWork private val coroutineScope: CoroutineScope,
@Assisted private val userWallet: UserWallet,
@Assisted private val cryptoCurrency: CryptoCurrency,
) {
fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(
userWalletId = userWallet.walletId,
networks = setOf(cryptoCurrency.network),
)
},
// we should update tx history and network for new balances
async {
updateTxHistory()
},
async {
updateNetworkStatuses()
},
).awaitAll()
}
}
private suspend fun updateNetworkStatuses(delay: Long = BALANCE_UPDATE_DELAY) {
updateDelayedNetworkStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = delay,
refresh = true,
)
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
txHistoryItemsCountEither.onRight {
if (txHistoryFeatureToggles.isFeatureEnabled) {
txHistoryContentUpdateEmitter.triggerUpdate()
} else {
getTxHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
refresh = true,
)
}
}
}
private companion object {
const val BALANCE_UPDATE_DELAY = 11_000L
}
@AssistedFactory
interface Factory {
fun create(cryptoCurrency: CryptoCurrency, userWallet: UserWallet): SendBalanceUpdater
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.send.v2.common
import com.tangem.features.send.v2.common.ui.state.NavigationUM
internal interface SendNavigationModelCallback {
fun onNavigationResult(navigationUM: NavigationUM)
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.ui
package com.tangem.features.send.v2.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -13,12 +13,14 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.ui.state.NavigationUM
@Composable
internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, ComposableContentComponent>) {
internal fun SendContent(
navigationUM: NavigationUM,
stackState: ChildStack<CommonSendRoute, ComposableContentComponent>,
) {
Column(
modifier = Modifier.Companion
.background(color = TangemTheme.colors.background.tertiary)
@ -27,7 +29,7 @@ internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, Compos
.systemBarsPadding(),
horizontalAlignment = Alignment.Companion.CenterHorizontally,
) {
SendAppBar(navigationUM = state.navigationUM)
SendAppBar(navigationUM = navigationUM)
Children(
stack = stackState,
animation = stackAnimation(slide()),
@ -35,7 +37,7 @@ internal fun SendContent(state: SendUM, stackState: ChildStack<SendRoute, Compos
) {
it.instance.Content(Modifier.weight(1f))
}
SendNavigationButtons(navigationUM = state.navigationUM)
SendNavigationButtons(navigationUM = navigationUM)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.ui
package com.tangem.features.send.v2.common.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
@ -29,7 +29,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
@Composable
internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) {

View file

@ -0,0 +1,62 @@
package com.tangem.features.send.v2.common.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import kotlinx.coroutines.delay
private const val TAP_HELP_KEY = "TAP_HELP_KEY"
private const val TAP_HELP_ANIMATION_DELAY = 500L
internal fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) {
item(key = TAP_HELP_KEY) {
var wrappedIsDisplay by remember { mutableStateOf(false) }
LaunchedEffect(key1 = isDisplay) {
delay(TAP_HELP_ANIMATION_DELAY)
wrappedIsDisplay = isDisplay
}
if (wrappedIsDisplay) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.animateItem()
.padding(top = TangemTheme.dimens.spacing20),
) {
val background = TangemTheme.colors.button.secondary
Icon(
painter = painterResource(id = R.drawable.ic_send_hint_shape_12),
tint = TangemTheme.colors.button.secondary,
contentDescription = null,
modifier = Modifier,
)
Text(
text = stringResourceSafe(id = R.string.send_summary_tap_hint),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(background)
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing12,
),
)
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.send.confirm.ui.state
package com.tangem.features.send.v2.common.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.common
package com.tangem.features.send.v2.common.ui.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable

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
@ -16,12 +14,7 @@ import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface SendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
internal interface CommonSendModelModule {
@Binds
@IntoMap
@ -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

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

@ -19,11 +19,13 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.ui.SendContent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.model.SendModel
import com.tangem.features.send.v2.send.ui.SendContent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
@ -44,17 +46,17 @@ internal class DefaultSendComponent @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : SendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<SendRoute>()
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val innerRouter = InnerRouter<SendRoute>(
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val initialRoute = if (params.amount == null) {
SendRoute.Destination(isEditMode = false)
CommonSendRoute.Destination(isEditMode = false)
} else {
SendRoute.Empty
CommonSendRoute.Empty
}
private val currentRoute = MutableStateFlow(initialRoute)
@ -112,47 +114,61 @@ internal class DefaultSendComponent @AssistedInject constructor(
BackHandler(onBack = ::onChildBack)
SendContent(
state = state,
navigationUM = state.navigationUM,
stackState = stackState,
)
}
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
SendRoute.Empty -> getStubComponent()
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
is SendRoute.Amount -> getAmountComponent(factoryContext, route)
is SendRoute.Fee -> getFeeComponent(factoryContext)
SendRoute.Confirm -> getConfirmComponent(factoryContext)
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
CommonSendRoute.Empty -> getStubComponent()
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route)
is CommonSendRoute.Amount -> getAmountComponent(factoryContext, route)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
}
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) =
SendDestinationComponent(
appComponentContext = factoryContext,
params = SendDestinationComponentParams.DestinationParams(
state = model.uiState.value.destinationUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
callback = model,
isEditMode = route.isEditMode,
onBackClick = ::onChildBack,
onNextClick = {
if (route.isEditMode) {
onChildBack()
} else {
innerRouter.push(CommonSendRoute.Amount(isEditMode = false))
}
},
),
)
private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent(
private fun getAmountComponent(factoryContext: AppComponentContext, route: CommonSendRoute) = SendAmountComponent(
appComponentContext = factoryContext,
params = SendAmountComponentParams.AmountParams(
state = model.uiState.value.amountUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Amount>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Amount>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
appCurrency = model.appCurrency,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
callback = model,
isEditMode = route.isEditMode,
predefinedAmountValue = model.predefinedAmountValue,
predefinedValues = model.predefinedValues,
onBackClick = ::onChildBack,
onNextClick = {
if (route.isEditMode) {
onChildBack()
} else {
innerRouter.push(CommonSendRoute.Confirm)
}
},
),
)
@ -165,7 +181,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = currentRoute.filterIsInstance<SendRoute.Fee>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
@ -174,6 +190,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
sendAmount = sendAmount,
destinationAddress = destinationAddress,
callback = model,
onNextClick = ::onChildBack,
),
)
} else {
@ -187,21 +204,21 @@ internal class DefaultSendComponent @AssistedInject constructor(
val predefinedAddress = params.destinationAddress
val predefinedValues =
if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) {
SendConfirmComponent.Params.PredefinedValues.Content(
PredefinedValues.Content.Deeplink(
amount = predefinedAmount,
address = predefinedAddress,
tag = params.tag,
memo = params.tag,
transactionId = predefinedTxId,
)
} else {
SendConfirmComponent.Params.PredefinedValues.Empty
PredefinedValues.Empty
}
return SendConfirmComponent(
appComponentContext = factoryContext,
params = SendConfirmComponent.Params(
state = model.uiState.value,
userWallet = model.userWallet,
currentRoute = currentRoute.filterIsInstance<SendRoute.Confirm>(),
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
@ -216,7 +233,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {
val isEmptyRoute = childStack.value.active.configuration == SendRoute.Empty
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.Empty
val isEmptyStack = childStack.value.backStack.isEmpty()
val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success

View file

@ -11,18 +11,20 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.*
@ -38,15 +40,13 @@ internal class SendConfirmComponent(
private val destinationBlockComponent =
SendDestinationBlockComponent(
appComponentContext = child("sendConfirmDestinationBlock"),
params = SendDestinationComponentParams.DestinationBlockParams(
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address,
predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag,
predefinedValues = params.predefinedValues,
),
onResult = model::onDestinationResult,
onClick = model::showEditDestination,
@ -61,8 +61,7 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
predefinedValues = params.predefinedValues,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,
@ -77,8 +76,8 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = model.enteredAmount.orZero(),
destinationAddress = model.enteredDestination.orEmpty(),
sendAmount = model.confirmData.enteredAmount.orZero(),
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
),
onResult = model::onFeeResult,
@ -93,13 +92,15 @@ internal class SendConfirmComponent(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
destinationAddress = model.enteredDestination.orEmpty(),
memo = model.enteredMemo,
amountValue = model.enteredAmount.orZero(),
reduceAmountBy = model.reduceAmountBy.orZero(),
isIgnoreReduce = model.isIgnoreReduce,
fee = model.fee,
feeError = model.feeError,
notificationData = NotificationData(
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
memo = model.confirmData.enteredMemo,
amountValue = model.confirmData.enteredAmount.orZero(),
reduceAmountBy = model.confirmData.reduceAmountBy.orZero(),
isIgnoreReduce = model.confirmData.isIgnoreReduce,
fee = model.confirmData.fee,
feeError = model.confirmData.feeError,
),
),
)
@ -140,20 +141,10 @@ internal class SendConfirmComponent(
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrency: AppCurrency,
val callback: ModelCallback,
val currentRoute: Flow<SendRoute.Confirm>,
val currentRoute: Flow<CommonSendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
) {
sealed class PredefinedValues {
data object Empty : PredefinedValues()
data class Content(
val transactionId: String,
val amount: String,
val address: String,
val tag: String?,
) : PredefinedValues()
}
}
)
interface ModelCallback {
fun onResult(sendUM: SendUM)

View file

@ -0,0 +1,15 @@
package com.tangem.features.send.v2.send.confirm.model
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.transaction.error.GetFeeError
import java.math.BigDecimal
data class ConfirmData(
val enteredAmount: BigDecimal?,
val reduceAmountBy: BigDecimal,
val isIgnoreReduce: Boolean,
val enteredDestination: String?,
val enteredMemo: String?,
val fee: Fee?,
val feeError: GetFeeError?,
)

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -25,20 +24,17 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
import com.tangem.domain.settings.NeverShowTapHelpUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.SendBalanceUpdater
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper
@ -47,7 +43,6 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmIn
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
@ -58,17 +53,13 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import com.tangem.features.txhistory.TxHistoryFeatureToggles
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.DelayedWork
import com.tangem.utils.extensions.orZero
import com.tangem.utils.extensions.stripZeroPlainString
import com.tangem.utils.transformer.update
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -89,21 +80,15 @@ internal class SendConfirmModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
private val sendFeeCheckReloadListener: SendFeeCheckReloadListener,
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
private val alertFactory: SendConfirmAlertFactory,
private val sendAnalyticHelper: SendAnalyticHelper,
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
@DelayedWork private val coroutineScope: CoroutineScope,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
sendBalanceUpdaterFactory: SendBalanceUpdater.Factory,
) : Model(), SendConfirmClickIntents {
private val params: SendConfirmComponent.Params = paramsContainer.require()
@ -113,6 +98,8 @@ internal class SendConfirmModel @Inject constructor(
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
private val cryptoCurrency = cryptoCurrencyStatus.currency
private val sendBalanceUpdater = sendBalanceUpdaterFactory.create(cryptoCurrency, userWallet)
private val _uiState = MutableStateFlow(params.state)
val uiState = _uiState.asStateFlow()
@ -125,20 +112,16 @@ internal class SendConfirmModel @Inject constructor(
private val feeSelectorUM
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
val enteredAmount: BigDecimal?
get() = amountState?.amountTextField?.cryptoAmount?.value
val reduceAmountBy: BigDecimal
get() = amountState?.reduceAmountBy.orZero()
val isIgnoreReduce: Boolean
get() = amountState?.isIgnoreReduce == true
val enteredDestination: String?
get() = destinationUM?.addressTextField?.value
val enteredMemo: String?
get() = destinationUM?.memoTextField?.value
val fee: Fee?
get() = feeSelectorUM?.selectedFee
val feeError: GetFeeError?
get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
val confirmData: ConfirmData
get() = ConfirmData(
enteredAmount = amountState?.amountTextField?.cryptoAmount?.value,
enteredMemo = destinationUM?.memoTextField?.value,
reduceAmountBy = amountState?.reduceAmountBy.orZero(),
isIgnoreReduce = amountState?.isIgnoreReduce == true,
enteredDestination = destinationUM?.addressTextField?.value,
fee = feeSelectorUM?.selectedFee,
feeError = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error,
)
private var sendIdleTimer: Long = 0L
private var isAmountSubtractAvailable = false
@ -183,7 +166,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
router.push(SendRoute.Destination(isEditMode = true))
router.push(CommonSendRoute.Destination(isEditMode = true))
}
}
@ -195,7 +178,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
router.push(SendRoute.Amount(isEditMode = true))
router.push(CommonSendRoute.Amount(isEditMode = true))
}
}
@ -207,7 +190,7 @@ internal class SendConfirmModel @Inject constructor(
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
}
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
router.push(SendRoute.Fee())
router.push(CommonSendRoute.Fee)
}
}
@ -236,15 +219,15 @@ internal class SendConfirmModel @Inject constructor(
override fun onFailedTxEmailClick(errorMessage: String) {
val amountValue = amountState?.amountTextField?.cryptoAmount?.value
val feeValue = fee?.amount?.value
val feeValue = confirmData.fee?.amount?.value
val receivingAmount = if (amountValue != null && feeValue != null) {
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = enteredAmount.orZero(),
amountValue = confirmData.enteredAmount.orZero(),
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
reduceAmountBy = confirmData.reduceAmountBy,
)
} else {
null
@ -257,7 +240,7 @@ internal class SendConfirmModel @Inject constructor(
errorMessage = errorMessage,
blockchainId = cryptoCurrency.network.id.value,
derivationPath = cryptoCurrency.network.derivationPath.value,
destinationAddress = enteredDestination.orEmpty(),
destinationAddress = confirmData.enteredDestination.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
@ -322,7 +305,7 @@ internal class SendConfirmModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy.orZero(),
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
)
modelScope.launch {
@ -369,7 +352,7 @@ internal class SendConfirmModel @Inject constructor(
ifRight = {
updateTransactionStatus(txData)
addTokenToWalletIfNeeded()
scheduleUpdates()
sendBalanceUpdater.scheduleUpdates()
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
},
)
@ -380,7 +363,7 @@ internal class SendConfirmModel @Inject constructor(
val wallets = destinationUM?.wallets ?: return
val receivingUserWallet = wallets
.firstOrNull { it.address == enteredDestination }
.firstOrNull { it.address == confirmData.enteredDestination }
?: return
val userWalletId = receivingUserWallet.userWalletId ?: return
@ -403,49 +386,6 @@ internal class SendConfirmModel @Inject constructor(
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))
}
private fun scheduleUpdates() {
coroutineScope.launch {
listOf(
// we should update network to find pending tx after 1 sec
async {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network))
},
// we should update tx history and network for new balance
async {
updateTxHistory()
},
async {
updateDelayedCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
delayMillis = BALANCE_UPDATE_DELAY,
refresh = true,
)
},
).awaitAll()
}
}
private suspend fun updateTxHistory() {
delay(BALANCE_UPDATE_DELAY)
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
txHistoryItemsCountEither.onRight {
if (txHistoryFeatureToggles.isFeatureEnabled) {
txHistoryContentUpdateEmitter.triggerUpdate()
} else {
getTxHistoryItemsUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
refresh = true,
)
}
}
}
private fun subscribeOnCheckFeeResultUpdates() {
sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess ->
if (isFeeResultSuccess) {
@ -462,13 +402,13 @@ internal class SendConfirmModel @Inject constructor(
modelScope.launch {
notificationsUpdateTrigger.triggerUpdate(
data = NotificationData(
destinationAddress = enteredDestination.orEmpty(),
memo = enteredMemo,
amountValue = enteredAmount.orZero(),
reduceAmountBy = reduceAmountBy.orZero(),
isIgnoreReduce = isIgnoreReduce,
fee = fee,
feeError = feeError,
destinationAddress = confirmData.enteredDestination.orEmpty(),
memo = confirmData.enteredMemo,
amountValue = confirmData.enteredAmount.orZero(),
reduceAmountBy = confirmData.reduceAmountBy.orZero(),
isIgnoreReduce = confirmData.isIgnoreReduce,
fee = confirmData.fee,
feeError = confirmData.feeError,
),
)
_uiState.update {
@ -554,6 +494,5 @@ internal class SendConfirmModel @Inject constructor(
private companion object {
const val CHECK_FEE_UPDATE_DELAY = 10_000L
const val BALANCE_UPDATE_DELAY = 11_000L
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf

View file

@ -1,6 +1,6 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.utils.transformer.Transformer

View file

@ -1,13 +1,13 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.TransactionData
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.utils.transformer.Transformer
internal class SendConfirmSentStateTransformer(
val txData: TransactionData.Uncompiled,
val txUrl: String,
private val txData: TransactionData.Uncompiled,
private val txUrl: String,
) : Transformer<SendUM> {
override fun transform(prevState: SendUM): SendUM {
return prevState.copy(

View file

@ -16,7 +16,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType

View file

@ -5,20 +5,15 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
@ -26,12 +21,16 @@ import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.tapHelp
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
@ -39,11 +38,8 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.notifications
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay
private const val TAP_HELP_KEY = "TAP_HELP_KEY"
private const val BLOCKS_KEY = "BLOCKS_KEY"
private const val TAP_HELP_ANIMATION_DELAY = 500L
@Suppress("LongParameterList")
@Composable
@ -148,45 +144,4 @@ private fun LazyListScope.blocks(
feeBlockComponent.Content(modifier = Modifier)
}
}
}
private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) {
item(key = TAP_HELP_KEY) {
var wrappedIsDisplay by remember { mutableStateOf(false) }
LaunchedEffect(key1 = isDisplay) {
delay(TAP_HELP_ANIMATION_DELAY)
wrappedIsDisplay = isDisplay
}
if (wrappedIsDisplay) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.animateItem()
.padding(top = TangemTheme.dimens.spacing20),
) {
val background = TangemTheme.colors.button.secondary
Icon(
painter = painterResource(id = R.drawable.ic_send_hint_shape_12),
tint = TangemTheme.colors.button.secondary,
contentDescription = null,
modifier = Modifier,
)
Text(
text = stringResourceSafe(id = R.string.send_summary_tap_hint),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(background)
.padding(
horizontal = TangemTheme.dimens.spacing14,
vertical = TangemTheme.dimens.spacing12,
),
)
}
}
}
}

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 CommonSendModelModule {
@Binds
@IntoMap
@ClassKey(SendModel::class)
fun provideSendModel(model: SendModel): Model
@Binds
@IntoMap
@ClassKey(SendConfirmModel::class)
fun provideSendConfirmModel(model: SendConfirmModel): Model
}

View file

@ -29,11 +29,12 @@ 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.SendComponent
import com.tangem.features.send.v2.common.NavigationUM
import com.tangem.features.send.v2.send.SendRoute
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
@ -90,7 +91,7 @@ internal class SendModel @Inject constructor(
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
var appCurrency: AppCurrency = AppCurrency.Default
var predefinedAmountValue: String? = null
var predefinedValues: PredefinedValues = PredefinedValues.Empty
private var balanceHidingJobHolder = JobHolder()
@ -214,7 +215,7 @@ internal class SendModel @Inject constructor(
feeCryptoCurrencyStatus = feeCurrencyStatus
if (params.amount != null) {
router.replaceAll(SendRoute.Confirm)
router.replaceAll(CommonSendRoute.Confirm)
}
}
@ -227,7 +228,11 @@ internal class SendModel @Inject constructor(
private fun onQrCodeScanned(address: String) {
val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull()
predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals)
predefinedValues = PredefinedValues.Content.QrCode(
amount = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals).orEmpty(),
address = parsedQrCode?.address.orEmpty(),
memo = parsedQrCode?.memo,
)
}
private fun onFailedTxEmailClick(errorMessage: String? = null) {

View file

@ -1,8 +1,8 @@
package com.tangem.features.send.v2.send.ui.state
import com.tangem.common.ui.amountScreen.models.AmountState
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.common.ui.state.NavigationUM
import com.tangem.features.send.v2.common.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

View file

@ -0,0 +1,200 @@
package com.tangem.features.send.v2.sendnft
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.value.ObserveLifecycleMode
import com.arkivanov.decompose.value.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.api.NFTSendComponent
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.ui.SendContent
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams
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.SendFeeComponentParams
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import java.math.BigDecimal
internal class DefaultNFTSendComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: NFTSendComponent.Params,
private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
) : NFTSendComponent, AppComponentContext by appComponentContext {
private val stackNavigation = StackNavigation<CommonSendRoute>()
private val innerRouter = InnerRouter<CommonSendRoute>(
stackNavigation = stackNavigation,
popCallback = { onChildBack() },
)
private val initialRoute = CommonSendRoute.Empty
private val currentRouteFlow = MutableStateFlow<CommonSendRoute>(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 ->
createChild(
configuration,
childByContext(
componentContext = factoryContext,
router = innerRouter,
),
)
},
)
init {
childStack.subscribe(
lifecycle = lifecycle,
mode = ObserveLifecycleMode.CREATE_DESTROY,
) { stack ->
componentScope.launch {
when (val activeComponent = stack.active.instance) {
is NFTSendConfirmComponent -> if (currentRouteFlow.value.isEditMode) {
// analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
activeComponent.updateState(model.uiState.value)
}
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)
SendContent(
navigationUM = state.navigationUM,
stackState = stackState,
)
}
private fun createChild(route: CommonSendRoute, factoryContext: AppComponentContext) = when (route) {
is CommonSendRoute.Destination -> getDestinationComponent(factoryContext, route)
is CommonSendRoute.Fee -> getFeeComponent(factoryContext)
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
else -> getStubComponent()
}
private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) =
SendDestinationComponent(
appComponentContext = factoryContext,
params = SendDestinationComponentParams.DestinationParams(
state = model.uiState.value.destinationUM,
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
analyticsCategoryName = "", // todo
userWalletId = params.userWalletId,
cryptoCurrency = model.cryptoCurrency,
callback = model,
onBackClick = ::onChildBack,
onNextClick = {
if (route.isEditMode) {
onChildBack()
} else {
innerRouter.push(CommonSendRoute.Confirm)
}
},
),
)
private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val state = model.uiState.value
val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value
return if (destinationAddress != null) {
SendFeeComponent(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(
state = model.uiState.value.feeUM,
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Fee>(),
analyticsCategoryName = "", // todo
userWallet = model.userWallet,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,
sendAmount = BigDecimal.ZERO,
destinationAddress = destinationAddress,
callback = model,
onNextClick = ::onChildBack,
),
)
} else {
getStubComponent()
}
}
private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent(
appComponentContext = factoryContext,
nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory,
params = NFTSendConfirmComponent.Params(
state = model.uiState.value,
analyticsCategoryName = "", // todo
userWallet = model.userWallet,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus,
appCurrency = model.appCurrency,
callback = model,
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Confirm>(),
isBalanceHidingFlow = model.isBalanceHiddenFlow,
),
)
private fun getStubComponent() = ComposableContentComponent { }
private fun onChildBack() {
val isEmptyRoute = childStack.value.active.configuration == CommonSendRoute.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,147 @@
package com.tangem.features.send.v2.sendnft.confirm
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.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.nft.models.NFTAsset
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.nft.component.NFTDetailsBlockComponent
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel
import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent
import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
import kotlinx.coroutines.flow.*
import java.math.BigDecimal
internal class NFTSendConfirmComponent(
appComponentContext: AppComponentContext,
params: Params,
nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: NFTSendConfirmModel = getOrCreateModel(params = params)
private val blockClickEnableFlow = MutableStateFlow(false)
private val destinationBlockComponent =
SendDestinationBlockComponent(
appComponentContext = child("NFTSendConfirmDestinationBlock"),
params = DestinationBlockParams(
state = model.uiState.value.destinationUM,
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrency = params.cryptoCurrencyStatus.currency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
predefinedValues = PredefinedValues.Empty,
),
onResult = model::onDestinationResult,
onClick = model::showEditDestination,
)
private val feeBlockComponent = SendFeeBlockComponent(
appComponentContext = child("NFTSendConfirmFeeBlock"),
params = SendFeeComponentParams.FeeBlockParams(
state = model.uiState.value.feeUM,
analyticsCategoryName = params.analyticsCategoryName,
userWallet = params.userWallet,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
sendAmount = BigDecimal.ZERO,
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
),
onResult = model::onFeeResult,
onClick = model::showEditFee,
)
private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create(
context = child("NFTDetailsBlock"),
params = NFTDetailsBlockComponent.Params(
userWalletId = params.userWallet.walletId,
nftAsset = params.nftAsset,
nftCollectionName = params.nftCollectionName,
),
)
private val notificationsComponent = NotificationsComponent(
appComponentContext = child("NFTSendConfirmNotifications"),
params = NotificationsComponent.Params(
analyticsCategoryName = params.analyticsCategoryName,
userWalletId = params.userWallet.walletId,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
appCurrency = params.appCurrency,
notificationData = NotificationData(
destinationAddress = model.confirmData.enteredDestination.orEmpty(),
memo = model.confirmData.enteredMemo,
amountValue = BigDecimal.ZERO,
reduceAmountBy = BigDecimal.ZERO,
isIgnoreReduce = false,
fee = model.confirmData.fee,
feeError = model.confirmData.feeError,
),
),
)
init {
model.uiState.onEach { state ->
val confirmUM = state.confirmUM as? ConfirmUM.Content
blockClickEnableFlow.value = confirmUM?.isSending == false
}.launchIn(componentScope)
}
fun updateState(state: NFTSendUM) {
destinationBlockComponent.updateState(state.destinationUM)
feeBlockComponent.updateState(state.feeUM)
model.updateState(state)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val notificationState by notificationsComponent.state.collectAsStateWithLifecycle()
NFTSendConfirmContent(
nftSendUM = state,
destinationBlockComponent = destinationBlockComponent,
feeBlockComponent = feeBlockComponent,
nftDetailsBlockComponent = nftDetailsBlockComponent,
notificationsComponent = notificationsComponent,
notificationsUM = notificationState,
)
}
data class Params(
val state: NFTSendUM,
val analyticsCategoryName: String,
val userWallet: UserWallet,
val appCurrency: AppCurrency,
val nftAsset: NFTAsset,
val nftCollectionName: String,
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val callback: ModelCallback,
val currentRoute: Flow<CommonSendRoute.Confirm>,
val isBalanceHidingFlow: StateFlow<Boolean>,
)
interface ModelCallback {
fun onResult(nftSendUM: NFTSendUM)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.send.v2.sendnft.confirm.model
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.transaction.error.GetFeeError
data class ConfirmData(
val enteredDestination: String?,
val enteredMemo: String?,
val fee: Fee?,
val feeError: GetFeeError?,
)

View file

@ -0,0 +1,16 @@
package com.tangem.features.send.v2.sendnft.confirm.model
internal interface NFTSendConfirmClickIntents {
fun showEditDestination()
fun showEditFee()
fun onSendClick()
fun onExploreClick()
fun onShareClick()
fun onFailedTxEmailClick(errorMessage: String)
}

Some files were not shown because too many files have changed in this diff Show more