Updated on 2026-08-14
This commit is contained in:
commit
5223d4b417
784 changed files with 24026 additions and 3170 deletions
|
|
@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import com.tangem.domain.redux.ReduxStateHolder
|
|||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.SaveWalletError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
|
||||
import com.tangem.features.details.impl.R
|
||||
|
|
@ -38,7 +37,7 @@ import kotlin.coroutines.resume
|
|||
internal class UserWalletSaver @Inject constructor(
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val messageSender: UiMessageSender,
|
||||
|
|
@ -56,17 +55,20 @@ internal class UserWalletSaver @Inject constructor(
|
|||
}
|
||||
},
|
||||
ifRight = { scanResponse ->
|
||||
recover(block = {
|
||||
scanResponse ?: return
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWallet(userWallet)
|
||||
}, recover = {
|
||||
recover(
|
||||
block = {
|
||||
scanResponse ?: return
|
||||
val userWallet = createUserWallet(scanResponse)
|
||||
saveWallet(userWallet)
|
||||
},
|
||||
recover = {
|
||||
val message = it.message
|
||||
|
||||
if (!message.isNullOrEmpty()) {
|
||||
messageSender.send(SnackbarMessage(message))
|
||||
}
|
||||
},)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -110,7 +112,7 @@ internal class UserWalletSaver @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
|
||||
val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build()
|
||||
val userWallet = userWalletBuilderFactory.create(scanResponse = response).build()
|
||||
|
||||
return ensureNotNull(userWallet) { Error.Unknown }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.features.managetokens.utils.list
|
||||
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import androidx.compose.ui.util.fastMap
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -10,10 +8,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase
|
||||
import com.tangem.domain.managetokens.GetManagedTokensUseCase
|
||||
import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase
|
||||
import com.tangem.domain.managetokens.*
|
||||
import com.tangem.domain.managetokens.model.*
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
|
@ -43,6 +38,7 @@ import javax.inject.Inject
|
|||
@ModelScoped
|
||||
internal class ManageTokensListManager @Inject constructor(
|
||||
private val getManagedTokensUseCase: GetManagedTokensUseCase,
|
||||
private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase,
|
||||
private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase,
|
||||
private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase,
|
||||
private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase,
|
||||
|
|
@ -166,47 +162,27 @@ internal class ManageTokensListManager @Inject constructor(
|
|||
return
|
||||
}
|
||||
|
||||
state.update { state ->
|
||||
val newBatches = distinctCurrencies(batchListState.data)
|
||||
val currentBatches = state.currencyBatches
|
||||
scope.launch {
|
||||
state.update { state ->
|
||||
val newBatches = getDistinctManagedTokensUseCase(batchListState.data)
|
||||
val currentBatches = state.currencyBatches
|
||||
|
||||
// Distinct until changed
|
||||
if (newBatches.size == currentBatches.size &&
|
||||
newBatches.map { it.key } == currentBatches.map { it.key } &&
|
||||
newBatches.flatMap { it.data } == currentBatches.flatMap { it.data }
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val canEditItems = userWalletId != null
|
||||
state.copy(
|
||||
userWalletId = userWalletId,
|
||||
currencyBatches = newBatches,
|
||||
uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems),
|
||||
canEditItems = canEditItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Add interception functionality to BatchFlow state and do this on domain
|
||||
// [REDACTED_JIRA]
|
||||
private fun distinctCurrencies(
|
||||
batches: List<Batch<Int, List<ManagedCryptoCurrency>>>,
|
||||
): List<Batch<Int, List<ManagedCryptoCurrency>>> {
|
||||
val allCurrenciesIds = mutableListOf<ManagedCryptoCurrency.ID>()
|
||||
|
||||
return batches.fastMap { batch ->
|
||||
val batchCurrencies = batch.data.toMutableList()
|
||||
|
||||
batch.data.fastForEachIndexed { index, currency ->
|
||||
if (currency.id in allCurrenciesIds) {
|
||||
batchCurrencies.removeAt(index)
|
||||
} else {
|
||||
allCurrenciesIds.add(currency.id)
|
||||
// Distinct until changed
|
||||
if (newBatches.size == currentBatches.size &&
|
||||
newBatches.map { it.key } == currentBatches.map { it.key } &&
|
||||
newBatches.flatMap { it.data } == currentBatches.flatMap { it.data }
|
||||
) {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
batch.copy(data = batchCurrencies)
|
||||
val canEditItems = userWalletId != null
|
||||
state.copy(
|
||||
userWalletId = userWalletId,
|
||||
currencyBatches = newBatches,
|
||||
uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems),
|
||||
canEditItems = canEditItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import com.tangem.core.ui.components.SpacerH12
|
|||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.audits.AuditLabelUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook
|
|||
import com.tangem.common.ui.charts.state.rememberMarketChartState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
|
||||
@Composable
|
||||
internal fun MarketTokenDetailsChart(
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import androidx.compose.ui.util.fastForEachIndexed
|
|||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
|
||||
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
|
||||
internal fun LazyListScope.tokenMarketDetailsBody(
|
||||
state: MarketsTokenDetailsUM.Body,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import com.tangem.core.decompose.navigation.Router
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
|
|
@ -102,12 +102,12 @@ internal class TokenActionsHandler @AssistedInject constructor(
|
|||
}
|
||||
},
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
network = currency.network.name,
|
||||
addresses = networkAddress.availableAddresses
|
||||
.mapToAddressModels(currency)
|
||||
.toImmutableList(),
|
||||
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
),
|
||||
network = currency.network,
|
||||
networkAddress = networkAddress,
|
||||
showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import com.tangem.common.ui.userwallet.UserWalletItem
|
|||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.tangem.core.ui.components.RectangleShimmer
|
|||
import com.tangem.core.ui.components.SmallButtonShimmer
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SimpleSettingsRow
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight
|
|||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ internal class MarketsTokenItemConverter(
|
|||
TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
val scaled = percent.setScale(4, RoundingMode.HALF_UP)
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.list.InfiniteListHandler
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.inputrow.InputRowChecked
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
package com.tangem.features.markets.tokenlist.impl.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_loading_error_title),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.try_to_load_data_again_button_title),
|
||||
onClick = onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
UnableToLoadData(onRetryClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ dependencies {
|
|||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.nft.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface NFTCollectionsComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, NFTCollectionsComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.nft.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface NFTDetailsComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, NFTDetailsComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.nft.component
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface NFTReceiveComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, NFTReceiveComponent>
|
||||
}
|
||||
|
|
@ -26,6 +26,12 @@ dependencies {
|
|||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.nft)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,23 @@
|
|||
package com.tangem.features.nft
|
||||
|
||||
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.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.details.model.NFTDetailsModel
|
||||
import com.tangem.features.nft.receive.DefaultNFTReceiveComponent
|
||||
import com.tangem.features.nft.receive.model.NFTReceiveModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -16,4 +29,37 @@ internal object NFTFeatureModule {
|
|||
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles {
|
||||
return DefaultNFTFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface NFTFeatureModuleBinds {
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindNFTCollectionsComponentFactory(
|
||||
impl: DefaultNFTCollectionsComponent.Factory,
|
||||
): NFTCollectionsComponent.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
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.nft.collections
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.nft.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(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: NFTCollectionsComponent.Params,
|
||||
) : NFTCollectionsComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTCollectionsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
NFTCollections(state, modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : NFTCollectionsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: NFTCollectionsComponent.Params,
|
||||
): DefaultNFTCollectionsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
internal data class NFTCollectionAssetUM(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val imageUrl: String?,
|
||||
val price: NFTSalePriceUM,
|
||||
val onItemClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class NFTCollectionAssetsListUM {
|
||||
data object Init : NFTCollectionAssetsListUM()
|
||||
data class Loading(val itemsCount: Int) : NFTCollectionAssetsListUM()
|
||||
data class Failed(val onRetryClick: () -> Unit) : NFTCollectionAssetsListUM()
|
||||
data class Content(val items: ImmutableList<NFTCollectionAssetUM>) : NFTCollectionAssetsListUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class NFTCollectionUM(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@DrawableRes val networkIconId: Int,
|
||||
val logoUrl: String?,
|
||||
val description: TextReference,
|
||||
val assets: NFTCollectionAssetsListUM,
|
||||
val isExpanded: Boolean,
|
||||
val onExpandClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
internal data class NFTCollectionsStateUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val content: NFTCollectionsUM,
|
||||
)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class NFTCollectionsUM {
|
||||
|
||||
data class Empty(
|
||||
val onReceiveClick: () -> Unit,
|
||||
) : NFTCollectionsUM()
|
||||
|
||||
data class Loading(
|
||||
val onReceiveClick: () -> Unit,
|
||||
) : NFTCollectionsUM()
|
||||
|
||||
data class Failed(
|
||||
val onRetryClick: () -> Unit,
|
||||
val onReceiveClick: () -> Unit,
|
||||
) : NFTCollectionsUM()
|
||||
|
||||
data class Content(
|
||||
val search: SearchBarUM,
|
||||
val collections: ImmutableList<NFTCollectionUM>,
|
||||
val warnings: ImmutableList<NFTCollectionsWarningUM>,
|
||||
val onReceiveClick: () -> Unit,
|
||||
) : NFTCollectionsUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
|
||||
internal data class NFTCollectionsWarningUM(
|
||||
val id: String,
|
||||
val config: NotificationConfig,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.nft.collections.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed class NFTSalePriceUM {
|
||||
data object Loading : NFTSalePriceUM()
|
||||
data object Failed : NFTSalePriceUM()
|
||||
data class Content(val price: String) : NFTSalePriceUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.domain.nft.models.NFTCollection
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class ChangeCollectionExpandedStateTransformer(
|
||||
private val collectionId: NFTCollection.Identifier,
|
||||
private val onFirstExpanded: () -> Unit,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (prevState.content) {
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> prevState.content
|
||||
is NFTCollectionsUM.Content -> prevState.content.copy(
|
||||
collections = prevState.content.collections.map {
|
||||
if (it.id == collectionId.toString()) {
|
||||
if (!it.isExpanded) {
|
||||
onFirstExpanded()
|
||||
}
|
||||
it.copy(isExpanded = !it.isExpanded)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.toPersistentList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (val content = prevState.content) {
|
||||
is NFTCollectionsUM.Content -> content.copy(
|
||||
search = content.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> content
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.nft.models.*
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class UpdateDataStateTransformer(
|
||||
private val nftCollections: List<NFTCollections>,
|
||||
private val searchQuery: String,
|
||||
private val onReceiveClick: () -> Unit,
|
||||
private val onRetryClick: () -> Unit,
|
||||
private val onExpandCollectionClick: (NFTCollection) -> Unit,
|
||||
private val onRetryAssetsClick: (NFTCollection) -> Unit,
|
||||
private val onAssetClick: (NFTAsset) -> Unit,
|
||||
private val initialSearchBarFactory: () -> SearchBarUM,
|
||||
) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when {
|
||||
nftCollections.allCollectionsFailed() ->
|
||||
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
|
||||
nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Failed(onRetryClick, onReceiveClick)
|
||||
nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Empty(onReceiveClick)
|
||||
!nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() ->
|
||||
NFTCollectionsUM.Loading(onReceiveClick)
|
||||
else -> {
|
||||
NFTCollectionsUM.Content(
|
||||
search = if (prevState.content is NFTCollectionsUM.Content) {
|
||||
prevState.content.search.copy(
|
||||
query = searchQuery,
|
||||
)
|
||||
} else {
|
||||
initialSearchBarFactory()
|
||||
},
|
||||
collections = nftCollections
|
||||
.map { it.content }
|
||||
.asSequence()
|
||||
.filterIsInstance<NFTCollections.Content.Collections>()
|
||||
.map { it.collections.orEmpty().transform(prevState, searchQuery) }
|
||||
.flatten()
|
||||
.toPersistentList(),
|
||||
warnings = transformNotifications(),
|
||||
onReceiveClick = onReceiveClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun List<NFTCollection>.transform(
|
||||
state: NFTCollectionsStateUM,
|
||||
query: String,
|
||||
): ImmutableList<NFTCollectionUM> = mapNotNull {
|
||||
if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) {
|
||||
NFTCollectionUM(
|
||||
id = it.id.toString(),
|
||||
networkIconId = getActiveIconRes(it.network.id.value),
|
||||
name = it.name.orEmpty(),
|
||||
description = TextReference.PluralRes(
|
||||
R.plurals.nft_collections_count,
|
||||
it.count,
|
||||
wrappedList(it.count),
|
||||
),
|
||||
logoUrl = it.logoUrl,
|
||||
assets = it.transformAssets(),
|
||||
onExpandClick = {
|
||||
onExpandCollectionClick(it)
|
||||
},
|
||||
isExpanded = it.isExpanded(state),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun transformNotifications(): ImmutableList<NFTCollectionsWarningUM> = buildList {
|
||||
if (nftCollections.anyCollectionFailed()) {
|
||||
add(
|
||||
NFTCollectionsWarningUM(
|
||||
id = "loading troubles",
|
||||
config = NotificationConfig(
|
||||
title = TextReference.Res(R.string.nft_collections_warning_title),
|
||||
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
private fun NFTCollection.transformAssets(): NFTCollectionAssetsListUM = when (val assets = this.assets) {
|
||||
is NFTCollection.Assets.Empty -> NFTCollectionAssetsListUM.Init
|
||||
is NFTCollection.Assets.Loading -> NFTCollectionAssetsListUM.Loading(count)
|
||||
is NFTCollection.Assets.Failed -> NFTCollectionAssetsListUM.Failed { onRetryAssetsClick(this) }
|
||||
is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content(
|
||||
items = assets
|
||||
.items
|
||||
.map { it.transform() }
|
||||
.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM(
|
||||
id = id.toString(),
|
||||
name = name.orEmpty(),
|
||||
imageUrl = media?.url,
|
||||
price = when (val salePrice = salePrice) {
|
||||
is NFTSalePrice.Empty -> NFTSalePriceUM.Failed
|
||||
is NFTSalePrice.Loading -> NFTSalePriceUM.Loading
|
||||
is NFTSalePrice.Error -> NFTSalePriceUM.Failed
|
||||
is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString())
|
||||
},
|
||||
onItemClick = {
|
||||
onAssetClick(this)
|
||||
},
|
||||
)
|
||||
|
||||
private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean =
|
||||
(state.content as? NFTCollectionsUM.Content)
|
||||
?.collections
|
||||
?.firstOrNull { it.id == id.toString() }
|
||||
?.isExpanded
|
||||
?: false
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.nft.collections.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer<NFTCollectionsStateUM> {
|
||||
|
||||
override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy(
|
||||
content = when (val content = prevState.content) {
|
||||
is NFTCollectionsUM.Content -> content.copy(
|
||||
search = content.search.copy(
|
||||
query = newQuery,
|
||||
),
|
||||
)
|
||||
is NFTCollectionsUM.Empty,
|
||||
is NFTCollectionsUM.Loading,
|
||||
is NFTCollectionsUM.Failed,
|
||||
-> content
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
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.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.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@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 _state = MutableStateFlow(
|
||||
value = NFTCollectionsStateUM(
|
||||
onBackClick = ::navigateBack,
|
||||
content = NFTCollectionsUM.Loading(::onReceiveClick),
|
||||
),
|
||||
)
|
||||
|
||||
private val params: NFTCollectionsComponent.Params = paramsContainer.require()
|
||||
|
||||
init {
|
||||
subscribeToNFTCollections()
|
||||
}
|
||||
|
||||
private fun subscribeToNFTCollections() {
|
||||
combine(
|
||||
flow = getNFTCollectionsUseCase(params.userWalletId),
|
||||
flow2 = searchManager.query.distinctUntilChanged(),
|
||||
) { nftCollections, query ->
|
||||
_state.update {
|
||||
UpdateDataStateTransformer(
|
||||
nftCollections = nftCollections,
|
||||
searchQuery = query,
|
||||
onReceiveClick = ::onReceiveClick,
|
||||
onRetryClick = ::onRetryClick,
|
||||
onExpandCollectionClick = ::onExpandCollectionClick,
|
||||
onRetryAssetsClick = ::onRetryAssetsClick,
|
||||
onAssetClick = ::onAssetClick,
|
||||
initialSearchBarFactory = ::getInitialSearchBar,
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
modelScope.launch {
|
||||
_state.update { UpdateSearchQueryTransformer(newQuery).transform(it) }
|
||||
|
||||
searchManager.update(newQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = ::onSearchQueryChange,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
)
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
_state.update {
|
||||
ToggleSearchBarTransformer(isActive).transform(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExpandCollectionClick(collection: NFTCollection) {
|
||||
_state.update {
|
||||
ChangeCollectionExpandedStateTransformer(
|
||||
collectionId = collection.id,
|
||||
onFirstExpanded = { onFirstExpanded(collection) },
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFirstExpanded(collection: NFTCollection) {
|
||||
loadCollectionAssets(collection)
|
||||
}
|
||||
|
||||
private fun onRetryAssetsClick(collection: NFTCollection) {
|
||||
loadCollectionAssets(collection)
|
||||
}
|
||||
|
||||
private fun onRetryClick() {
|
||||
// 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(
|
||||
userWalletId = params.userWalletId,
|
||||
network = collection.network,
|
||||
collectionId = collection.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
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
|
||||
import androidx.compose.material3.Text
|
||||
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.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
private const val CHEVRON_ROTATION_EXPANDED = 180f
|
||||
private const val CHEVRON_ROTATION_COLLAPSED = 0f
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) {
|
||||
val isExpanded = state.isExpanded
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = state.onExpandClick)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing16,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Logo(state)
|
||||
|
||||
Text(state)
|
||||
|
||||
val rotation by animateFloatAsState(
|
||||
targetValue = if (isExpanded) {
|
||||
CHEVRON_ROTATION_EXPANDED
|
||||
} else {
|
||||
CHEVRON_ROTATION_COLLAPSED
|
||||
},
|
||||
label = "chevron_rotation",
|
||||
)
|
||||
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.rotate(rotation)
|
||||
.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
tint = if (isExpanded) {
|
||||
TangemTheme.colors.icon.primary1
|
||||
} else {
|
||||
TangemTheme.colors.icon.inactive
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier,
|
||||
text = state.name,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
text = state.description.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollection(@PreviewParameter(NFTCollectionProvider::class) item: NFTCollectionUM) {
|
||||
TangemThemePreview {
|
||||
NFTCollection(
|
||||
state = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class NFTCollectionProvider : CollectionPreviewParameterProvider<NFTCollectionUM>(
|
||||
collection = listOf(
|
||||
NFTCollectionUM(
|
||||
id = "item1",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
|
||||
isExpanded = false,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
id = "item2",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Loading(
|
||||
itemsCount = 3,
|
||||
),
|
||||
onExpandClick = { },
|
||||
isExpanded = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.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.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH2
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM
|
||||
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(state.imageUrl)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(radius = TangemTheme.dimens.radius16)
|
||||
},
|
||||
error = {
|
||||
Placeholder()
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
SpacerH12()
|
||||
Text(
|
||||
text = state.name,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerH2()
|
||||
NFTSalePrice(
|
||||
state = state.price,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Placeholder(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.field.focused),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size52),
|
||||
painter = painterResource(R.drawable.ic_nft_placeholder_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 180, showBackground = true)
|
||||
@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollectionAsset(
|
||||
@PreviewParameter(NFTCollectionAssetProvider::class) state: NFTCollectionAssetUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
NFTCollectionAsset(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider<NFTCollectionAssetUM>(
|
||||
collection = listOf(
|
||||
NFTCollectionAssetUM(
|
||||
id = "item1",
|
||||
name = "Nethers #0853",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Loading,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTCollectionAssetUM(
|
||||
id = "item2",
|
||||
name = "Nethers #0854",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Failed,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTCollectionAssetUM(
|
||||
id = "item3",
|
||||
name = "Nethers #0855",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Content("0.05 ETH"),
|
||||
onItemClick = { },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollectionAssetLoading(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f),
|
||||
)
|
||||
SpacerH12()
|
||||
TextShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size86),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
SpacerH2()
|
||||
TextShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size48),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 180, showBackground = true)
|
||||
@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollectionAsset() {
|
||||
TangemThemePreview {
|
||||
NFTCollectionAssetLoading()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsWarningUM
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollectionWarning(state: NFTCollectionsWarningUM, modifier: Modifier = Modifier) {
|
||||
Notification(
|
||||
modifier = modifier,
|
||||
config = state.config,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackClick)
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
topBar = {
|
||||
AppBarWithBackButton(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
onBackClick = state.onBackClick,
|
||||
text = stringResourceSafe(id = R.string.nft_collections_title),
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
AnimatedContent(
|
||||
targetState = state.content,
|
||||
contentKey = { it::class },
|
||||
label = "NFT Collections",
|
||||
) {
|
||||
val contentModifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
when (val content = it) {
|
||||
is NFTCollectionsUM.Content -> NFTCollectionsContent(content, contentModifier)
|
||||
is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content, contentModifier)
|
||||
is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content, contentModifier)
|
||||
is NFTCollectionsUM.Loading -> Unit
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
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.util.fastForEach
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.collections.entity.*
|
||||
import com.tangem.features.nft.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
SearchBar(
|
||||
state = content.search,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
content.warnings.fastForEach {
|
||||
key(it.id) {
|
||||
NFTCollectionWarning(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
state = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing60,
|
||||
)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
state = listState,
|
||||
) {
|
||||
content.collections.fastForEach { collection ->
|
||||
item(key = collection.id) {
|
||||
NFTCollection(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = collection,
|
||||
)
|
||||
}
|
||||
when (val assets = collection.assets) {
|
||||
is NFTCollectionAssetsListUM.Init -> Unit
|
||||
is NFTCollectionAssetsListUM.Loading -> {
|
||||
assetsListLoading(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
is NFTCollectionAssetsListUM.Failed -> {
|
||||
assetsListFailed(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
is NFTCollectionAssetsListUM.Content -> {
|
||||
assetsListContent(
|
||||
collectionId = collection.id,
|
||||
content = assets,
|
||||
expanded = collection.isExpanded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = content.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListLoading(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Loading,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
val itemsCount = content.itemsCount
|
||||
val rowCount = (itemsCount + 1) / 2
|
||||
repeat(rowCount) { rowIndex ->
|
||||
item(
|
||||
key = "loading_${collectionId}_$rowIndex",
|
||||
) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
NFTCollectionAssetLoading(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
NFTCollectionAssetLoading(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListFailed(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Failed,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
item(
|
||||
key = "failed_$collectionId",
|
||||
) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size142)
|
||||
.animateContentSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(
|
||||
onRetryClick = content.onRetryClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.assetsListContent(
|
||||
collectionId: String,
|
||||
content: NFTCollectionAssetsListUM.Content,
|
||||
expanded: Boolean,
|
||||
) {
|
||||
val items = content.items
|
||||
val itemsCount = items.size
|
||||
val rowCount = (itemsCount + 1) / 2
|
||||
repeat(rowCount) { rowIndex ->
|
||||
val item1 = items[rowIndex * 2]
|
||||
val item2 = items.getOrNull(rowIndex * 2 + 1)
|
||||
item(
|
||||
key = "content_${collectionId}_${item1.id}_${item2?.id}",
|
||||
) {
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
val paddingValues = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing6,
|
||||
top = TangemTheme.dimens.spacing6,
|
||||
end = TangemTheme.dimens.spacing6,
|
||||
bottom = TangemTheme.dimens.spacing20,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing6)
|
||||
.animateContentSize(),
|
||||
) {
|
||||
NFTCollectionAsset(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable { item1.onItemClick() }
|
||||
.padding(paddingValues),
|
||||
state = item1,
|
||||
)
|
||||
if (item2 == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
NFTCollectionAsset(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.clickable { item2.onItemClick() }
|
||||
.padding(paddingValues),
|
||||
state = item2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollectionsContent() {
|
||||
TangemThemePreview {
|
||||
NFTCollectionsContent(
|
||||
content = NFTCollectionsUM.Content(
|
||||
search = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
collections = persistentListOf(
|
||||
NFTCollectionUM(
|
||||
id = "item1",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Content(persistentListOf()),
|
||||
isExpanded = false,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
id = "item2",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Loading(
|
||||
itemsCount = 1,
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
id = "item3",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Failed(
|
||||
onRetryClick = { },
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
NFTCollectionUM(
|
||||
id = "item4",
|
||||
name = "Nethers",
|
||||
logoUrl = "",
|
||||
networkIconId = R.drawable.img_eth_22,
|
||||
description = TextReference.Str("3 items"),
|
||||
assets = NFTCollectionAssetsListUM.Content(
|
||||
items = persistentListOf(
|
||||
NFTCollectionAssetUM(
|
||||
id = "item1",
|
||||
name = "Nethers #0854",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Content("0.05 ETH"),
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTCollectionAssetUM(
|
||||
id = "item2",
|
||||
name = "Nethers #0855",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Loading,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTCollectionAssetUM(
|
||||
id = "item3",
|
||||
name = "Nethers #0856",
|
||||
imageUrl = "img",
|
||||
price = NFTSalePriceUM.Failed,
|
||||
onItemClick = { },
|
||||
),
|
||||
),
|
||||
),
|
||||
isExpanded = true,
|
||||
onExpandClick = { },
|
||||
),
|
||||
),
|
||||
warnings = persistentListOf(
|
||||
NFTCollectionsWarningUM(
|
||||
id = "loading troubles",
|
||||
config = NotificationConfig(
|
||||
title = TextReference.Res(R.string.nft_collections_warning_title),
|
||||
subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle),
|
||||
iconResId = R.drawable.ic_alert_triangle_20,
|
||||
),
|
||||
),
|
||||
),
|
||||
onReceiveClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_nft_placeholder_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing24),
|
||||
text = stringResource(R.string.nft_collections_empty_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
text = stringResource(R.string.nft_collections_empty_description),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing48)
|
||||
.wrapContentWidth(),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = state.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollectionsEmpty() {
|
||||
TangemThemePreview {
|
||||
NFTCollectionsEmpty(
|
||||
state = NFTCollectionsUM.Empty(
|
||||
onReceiveClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.collections.entity.NFTCollectionsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
UnableToLoadData(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center),
|
||||
onRetryClick = state.onRetryClick,
|
||||
)
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter),
|
||||
text = stringResourceSafe(R.string.nft_collections_receive),
|
||||
onClick = state.onReceiveClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTCollectionsFailed() {
|
||||
TangemThemePreview {
|
||||
NFTCollectionsFailed(
|
||||
state = NFTCollectionsUM.Failed(
|
||||
onRetryClick = { },
|
||||
onReceiveClick = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.nft.collections.ui
|
||||
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.core.ui.components.TextShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.collections.entity.NFTSalePriceUM
|
||||
|
||||
@Composable
|
||||
internal fun NFTSalePrice(state: NFTSalePriceUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is NFTSalePriceUM.Content -> {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = state.price,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
is NFTSalePriceUM.Failed -> {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = "",
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
is NFTSalePriceUM.Loading -> {
|
||||
TextShimmer(
|
||||
modifier = modifier
|
||||
.width(TangemTheme.dimens.size48),
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.nft.details
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.nft.component.NFTDetailsComponent
|
||||
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(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: NFTDetailsComponent.Params,
|
||||
) : NFTDetailsComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTDetailsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
NFTDetails(state, modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : NFTDetailsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: NFTDetailsComponent.Params,
|
||||
): DefaultNFTDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.nft.details.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class NFTAssetUM(
|
||||
val name: String,
|
||||
val media: Media,
|
||||
val topInfo: TopInfo,
|
||||
val traits: ImmutableList<BlockItem>,
|
||||
val baseInfoItems: ImmutableList<BlockItem>,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
sealed class TopInfo {
|
||||
data object Empty : TopInfo()
|
||||
data class Content(
|
||||
val title: TextReference?,
|
||||
val salePrice: SalePrice,
|
||||
val description: String?,
|
||||
val rarity: Rarity,
|
||||
) : TopInfo()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class Media {
|
||||
data object Empty : Media()
|
||||
data class Content(
|
||||
val mimetype: String?,
|
||||
val url: String,
|
||||
) : Media()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class SalePrice {
|
||||
data object Loading : SalePrice()
|
||||
data object Empty : SalePrice()
|
||||
data class Content(val value: String, val fiatValue: String) : SalePrice()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed class Rarity {
|
||||
data object Empty : Rarity()
|
||||
data class Content(
|
||||
val rank: String,
|
||||
val label: String,
|
||||
val showDivider: Boolean,
|
||||
) : Rarity()
|
||||
}
|
||||
|
||||
data class BlockItem(
|
||||
val title: TextReference,
|
||||
val titleTextEllipsis: TextEllipsis = TextEllipsis.End,
|
||||
val value: String,
|
||||
val valueTextEllipsis: TextEllipsis = TextEllipsis.End,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.nft.details.entity
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
||||
internal data class NFTDetailsUM(
|
||||
val nftAsset: NFTAssetUM,
|
||||
val onBackClick: () -> Unit,
|
||||
val onReadMoreClick: () -> Unit,
|
||||
val onSeeAllClick: () -> Unit,
|
||||
val onExploreClick: () -> Unit,
|
||||
val onSendClick: () -> Unit,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
)
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.features.nft.details.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.features.nft.component.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 javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class NFTDetailsModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private val params: NFTDetailsComponent.Params = paramsContainer.require()
|
||||
|
||||
val state: StateFlow<NFTDetailsUM> get() = _state
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
value = NFTDetailsUM(
|
||||
nftAsset = NFTAssetUM(
|
||||
name = "",
|
||||
media = NFTAssetUM.Media.Empty,
|
||||
topInfo = NFTAssetUM.TopInfo.Empty,
|
||||
traits = persistentListOf(),
|
||||
baseInfoItems = persistentListOf(),
|
||||
),
|
||||
onBackClick = ::navigateBack,
|
||||
onReadMoreClick = ::onReadMoreClick,
|
||||
onSeeAllClick = ::onSeeAllClick,
|
||||
onExploreClick = ::onExploreClick,
|
||||
onSendClick = ::onSendClick,
|
||||
bottomSheetConfig = null,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onReadMoreClick() {
|
||||
// TODO implement
|
||||
}
|
||||
|
||||
private fun onSeeAllClick() {
|
||||
// TODO implement
|
||||
}
|
||||
|
||||
private fun onExploreClick() {
|
||||
// TODO implement
|
||||
}
|
||||
|
||||
private fun onSendClick() {
|
||||
// TODO implement
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
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.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.details.entity.NFTDetailsUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetails(state: NFTDetailsUM, 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 = state.nftAsset.name,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
NFTDetailsAsset(
|
||||
state = state.nftAsset,
|
||||
onReadMoreClick = state.onReadMoreClick,
|
||||
onSeeAllClick = state.onSeeAllClick,
|
||||
onExploreClick = state.onExploreClick,
|
||||
modifier = Modifier
|
||||
.padding(innerPadding),
|
||||
)
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
floatingActionButton = {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_send),
|
||||
onClick = state.onSendClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.bottomFade
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.NFTAssetUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsAsset(
|
||||
state: NFTAssetUM,
|
||||
onReadMoreClick: () -> Unit,
|
||||
onSeeAllClick: () -> Unit,
|
||||
onExploreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val bottomBarHeight = with(LocalDensity.current) {
|
||||
WindowInsets.systemBars.getBottom(density = this).toDp()
|
||||
}
|
||||
|
||||
val bottomPadding = TangemTheme.dimens.spacing76 + bottomBarHeight
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.bottomFade(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = bottomPadding,
|
||||
)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
NFTDetailsLogo(
|
||||
state = state.media,
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f),
|
||||
)
|
||||
NFTDetailsInfoGroup(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
state = state.topInfo,
|
||||
onReadMoreClick = onReadMoreClick,
|
||||
)
|
||||
NFTDetailsBlocksGroup(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
items = state.traits,
|
||||
title = resourceReference(R.string.nft_details_traits),
|
||||
action = {
|
||||
NFTBlocksGroupAction(
|
||||
text = resourceReference(R.string.common_see_all),
|
||||
startIcon = { },
|
||||
onClick = onSeeAllClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
NFTDetailsBlocksGroup(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
items = state.baseInfoItems,
|
||||
title = resourceReference(R.string.nft_details_base_information),
|
||||
action = {
|
||||
NFTBlocksGroupAction(
|
||||
text = resourceReference(R.string.common_explore),
|
||||
startIcon = {
|
||||
NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24)
|
||||
},
|
||||
onClick = onExploreClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 1000, showBackground = true)
|
||||
@Preview(widthDp = 360, heightDp = 1000, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetProvider::class) state: NFTAssetUM) {
|
||||
TangemThemePreview {
|
||||
NFTDetailsAsset(
|
||||
state = state,
|
||||
onReadMoreClick = { },
|
||||
onSeeAllClick = { },
|
||||
onExploreClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private class NFTAssetProvider : CollectionPreviewParameterProvider<NFTAssetUM>(
|
||||
collection = listOf(
|
||||
NFTAssetUM(
|
||||
name = "Nethers #9744",
|
||||
media = NFTAssetUM.Media.Content(
|
||||
url = "img1",
|
||||
mimetype = "image/jpeg",
|
||||
),
|
||||
topInfo = NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = true,
|
||||
),
|
||||
),
|
||||
traits = persistentListOf(
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Tier"),
|
||||
value = "Infinite",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Phygital Toy"),
|
||||
value = "None",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Class"),
|
||||
value = "CYBER",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Accessory"),
|
||||
value = "No accessory",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Sneakers"),
|
||||
value = "Boots",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Artist"),
|
||||
value = "DJ Dragoon",
|
||||
),
|
||||
),
|
||||
baseInfoItems = persistentListOf(
|
||||
NFTAssetUM.BlockItem(
|
||||
title = resourceReference(R.string.nft_details_token_standard),
|
||||
value = "ERC-721",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = resourceReference(R.string.nft_details_contract_address),
|
||||
value = "0x6811f2fgd892ac83f719",
|
||||
valueTextEllipsis = TextEllipsis.Middle,
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = resourceReference(R.string.nft_details_token_id),
|
||||
value = "100200273",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = resourceReference(R.string.nft_details_chain),
|
||||
value = "Ethereum",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.NFTAssetUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsBlocksGroup(
|
||||
title: TextReference,
|
||||
action: (@Composable BoxScope.() -> Unit)?,
|
||||
items: ImmutableList<NFTAssetUM.BlockItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val itemsCount = items.size
|
||||
if (items.size > 0) {
|
||||
val rowCount = (itemsCount + 1) / 2
|
||||
val paddingValues = PaddingValues(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
)
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = {
|
||||
NFTDetailsGroupTitle(
|
||||
text = title,
|
||||
)
|
||||
},
|
||||
action = action,
|
||||
contentHorizontalPadding = TangemTheme.dimens.spacing6,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
repeat(rowCount) { rowIndex ->
|
||||
val item1 = items[rowIndex * 2]
|
||||
val item2 = items.getOrNull(rowIndex * 2 + 1)
|
||||
key(rowIndex) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
NFTDetailsGroupBlock(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
title = item1.title,
|
||||
value = stringReference(item1.value),
|
||||
titleEllipsis = item1.titleTextEllipsis,
|
||||
valueEllipsis = item1.valueTextEllipsis,
|
||||
)
|
||||
if (item2 == null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
)
|
||||
} else {
|
||||
NFTDetailsGroupBlock(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(paddingValues),
|
||||
title = item2.title,
|
||||
value = stringReference(item2.value),
|
||||
titleEllipsis = item2.titleTextEllipsis,
|
||||
valueEllipsis = item2.valueTextEllipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsGroupTitle(text: TextReference, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = text.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsGroupBlock(
|
||||
title: TextReference,
|
||||
value: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
titleEllipsis: TextEllipsis = TextEllipsis.End,
|
||||
valueEllipsis: TextEllipsis = TextEllipsis.End,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
EllipsisText(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
ellipsis = titleEllipsis,
|
||||
)
|
||||
EllipsisText(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4),
|
||||
text = value.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = valueEllipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NFTBlocksGroupAction(
|
||||
text: TextReference,
|
||||
startIcon: @Composable RowScope.() -> Unit,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
startIcon()
|
||||
Text(
|
||||
text = text.resolveReference(),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NFTBlocksGroupActionIcon(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) {
|
||||
Icon(
|
||||
modifier = modifier.size(size = TangemTheme.dimens.size16),
|
||||
painter = painterResource(id = iconRes),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTDetailsInfoBlock(
|
||||
@PreviewParameter(NFTAssetBlocksProvider::class) state: ImmutableList<NFTAssetUM.BlockItem>,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
Column {
|
||||
NFTDetailsBlocksGroup(
|
||||
title = stringReference("Title"),
|
||||
action = null,
|
||||
items = state,
|
||||
)
|
||||
NFTDetailsBlocksGroup(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
title = stringReference("Title"),
|
||||
action = {
|
||||
NFTBlocksGroupAction(
|
||||
text = resourceReference(R.string.common_see_all),
|
||||
startIcon = { },
|
||||
onClick = { },
|
||||
)
|
||||
},
|
||||
items = state,
|
||||
)
|
||||
NFTDetailsBlocksGroup(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
title = stringReference("Title"),
|
||||
action = {
|
||||
NFTBlocksGroupAction(
|
||||
text = resourceReference(R.string.common_explore),
|
||||
startIcon = {
|
||||
NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24)
|
||||
},
|
||||
onClick = { },
|
||||
)
|
||||
},
|
||||
items = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NFTAssetBlocksProvider : CollectionPreviewParameterProvider<ImmutableList<NFTAssetUM.BlockItem>>(
|
||||
collection = listOf(
|
||||
persistentListOf(
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Tier"),
|
||||
value = "Infinite",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Phygital Toy"),
|
||||
value = "None",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Class"),
|
||||
value = "CYBER",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Accessory"),
|
||||
value = "No accessory",
|
||||
),
|
||||
NFTAssetUM.BlockItem(
|
||||
title = stringReference("Sneakers"),
|
||||
value = "Boots",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.TextShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.block.information.InformationBlock
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
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.NFTAssetUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsInfoGroup(
|
||||
state: NFTAssetUM.TopInfo,
|
||||
onReadMoreClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (state) {
|
||||
is NFTAssetUM.TopInfo.Empty -> Unit
|
||||
is NFTAssetUM.TopInfo.Content -> {
|
||||
InformationBlock(
|
||||
modifier = modifier,
|
||||
title = if (state.title != null) {
|
||||
{ NFTDetailsGroupTitle(state.title) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
contentHorizontalPadding = 0.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier,
|
||||
) {
|
||||
SalePrice(state.salePrice)
|
||||
Description(state.description, onReadMoreClick)
|
||||
Rarity(state.rarity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.fillMaxWidth().animateContentSize(),
|
||||
visible = state !is NFTAssetUM.SalePrice.Empty,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
when (state) {
|
||||
is NFTAssetUM.SalePrice.Empty -> Unit
|
||||
is NFTAssetUM.SalePrice.Loading -> {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
TextShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size158),
|
||||
style = TangemTheme.typography.head,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
TextShimmer(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4)
|
||||
.width(TangemTheme.dimens.size90),
|
||||
style = TangemTheme.typography.caption2,
|
||||
textSizeHeight = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
is NFTAssetUM.SalePrice.Content -> {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
Text(
|
||||
text = state.value,
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4),
|
||||
text = state.fiatValue,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Description(description: String?, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
if (!description.isNullOrEmpty()) {
|
||||
// TODO configure right way to display 3 rows and "Read more" label
|
||||
EllipsisText(
|
||||
modifier = modifier
|
||||
.clickable { onReadMoreClick() }
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
text = description,
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is NFTAssetUM.Rarity.Empty -> Unit
|
||||
is NFTAssetUM.Rarity.Content -> {
|
||||
if (state.showDivider) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
color = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
) {
|
||||
NFTDetailsGroupBlock(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
title = resourceReference(R.string.nft_details_rarity_label),
|
||||
value = stringReference(state.label),
|
||||
)
|
||||
NFTDetailsGroupBlock(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing6,
|
||||
vertical = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
title = resourceReference(R.string.nft_details_rarity_rank),
|
||||
value = stringReference(state.rank),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetInfoProvider::class) state: NFTAssetUM.TopInfo) {
|
||||
TangemThemePreview {
|
||||
NFTDetailsInfoGroup(
|
||||
state = state,
|
||||
onReadMoreClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
private class NFTAssetInfoProvider : CollectionPreviewParameterProvider<NFTAssetUM.TopInfo>(
|
||||
collection = listOf(
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = true,
|
||||
),
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Loading,
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = true,
|
||||
),
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
|
||||
description = null,
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = true,
|
||||
),
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Empty,
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = resourceReference(R.string.nft_details_last_sale_price),
|
||||
salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"),
|
||||
description = null,
|
||||
rarity = NFTAssetUM.Rarity.Empty,
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = null,
|
||||
salePrice = NFTAssetUM.SalePrice.Empty,
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = true,
|
||||
),
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = null,
|
||||
salePrice = NFTAssetUM.SalePrice.Empty,
|
||||
description = null,
|
||||
rarity = NFTAssetUM.Rarity.Content(
|
||||
rank = "Top 1% rarity",
|
||||
label = "115.28",
|
||||
showDivider = false,
|
||||
),
|
||||
),
|
||||
NFTAssetUM.TopInfo.Content(
|
||||
title = null,
|
||||
salePrice = NFTAssetUM.SalePrice.Empty,
|
||||
description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3),
|
||||
rarity = NFTAssetUM.Rarity.Empty,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.tangem.features.nft.details.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.details.entity.NFTAssetUM
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTDetailsLogo(state: NFTAssetUM.Media, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is NFTAssetUM.Media.Empty -> {
|
||||
Placeholder(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
is NFTAssetUM.Media.Content -> {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(state.url)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
loading = {
|
||||
RectangleShimmer(radius = TangemTheme.dimens.radius16)
|
||||
},
|
||||
error = {
|
||||
Placeholder()
|
||||
},
|
||||
contentScale = ContentScale.Crop,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Placeholder(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.field.focused),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size120),
|
||||
painter = painterResource(R.drawable.ic_nft_placeholder_76),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetMediaProvider::class) state: NFTAssetUM.Media) {
|
||||
TangemThemePreview {
|
||||
NFTDetailsLogo(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class NFTAssetMediaProvider : CollectionPreviewParameterProvider<NFTAssetUM.Media>(
|
||||
collection = listOf(
|
||||
NFTAssetUM.Media.Empty,
|
||||
NFTAssetUM.Media.Content(
|
||||
mimetype = "image/jpeg",
|
||||
url = "img1",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.nft.receive
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.nft.component.NFTReceiveComponent
|
||||
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(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: NFTReceiveComponent.Params,
|
||||
) : NFTReceiveComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTReceiveModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
NFTReceive(state, modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : NFTReceiveComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: NFTReceiveComponent.Params,
|
||||
): DefaultNFTReceiveComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.nft.receive.entity
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
internal data class NFTNetworkUM(
|
||||
val id: String,
|
||||
@DrawableRes val iconRes: Int,
|
||||
val name: String,
|
||||
val onItemClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.nft.receive.entity
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class NFTReceiveUM(
|
||||
val onBackClick: () -> Unit,
|
||||
val search: SearchBarUM,
|
||||
val networks: Networks,
|
||||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
sealed class Networks {
|
||||
data object Empty : Networks()
|
||||
|
||||
data class Content(
|
||||
val items: ImmutableList<NFTNetworkUM>,
|
||||
) : Networks()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.nft.receive.entity.transformer
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ShowReceiveBottomSheetTransformer(
|
||||
private val network: Network,
|
||||
private val networkAddress: NetworkAddress,
|
||||
private val onDismissBottomSheet: () -> Unit,
|
||||
private val onCopyClick: (String) -> Unit,
|
||||
private val onShareClick: (String) -> Unit,
|
||||
) : Transformer<NFTReceiveUM> {
|
||||
|
||||
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onDismissBottomSheet,
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
asset = TokenReceiveBottomSheetConfig.Asset.NFT,
|
||||
network = network,
|
||||
networkAddress = networkAddress,
|
||||
showMemoDisclaimer = network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = onCopyClick,
|
||||
onShareClick = onShareClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.nft.receive.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer<NFTReceiveUM> {
|
||||
|
||||
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
|
||||
search = prevState.search.copy(
|
||||
isActive = isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.nft.receive.entity.transformer
|
||||
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.nft.receive.entity.NFTNetworkUM
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class UpdateDataStateTransformer(
|
||||
private val networks: List<Network>,
|
||||
private val onNetworkClick: (Network) -> Unit,
|
||||
) : Transformer<NFTReceiveUM> {
|
||||
|
||||
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
|
||||
networks = when {
|
||||
networks.isEmpty() -> NFTReceiveUM.Networks.Empty
|
||||
else -> NFTReceiveUM.Networks.Content(
|
||||
items = networks
|
||||
.map { it.transform() }
|
||||
.toPersistentList(),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private fun Network.transform(): NFTNetworkUM = NFTNetworkUM(
|
||||
id = id.value,
|
||||
iconRes = getActiveIconRes(id.value),
|
||||
name = name,
|
||||
onItemClick = {
|
||||
onNetworkClick(this)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.nft.receive.entity.transformer
|
||||
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer<NFTReceiveUM> {
|
||||
|
||||
override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy(
|
||||
search = prevState.search.copy(
|
||||
query = newQuery,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
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
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase
|
||||
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.entity.NFTReceiveUM
|
||||
import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheetTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer
|
||||
import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@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,
|
||||
private val getNFTNetworkStatusUseCase: GetNFTNetworkStatusUseCase,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<NFTReceiveUM> get() = _state
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
value = NFTReceiveUM(
|
||||
onBackClick = ::navigateBack,
|
||||
search = getInitialSearchBar(),
|
||||
networks = NFTReceiveUM.Networks.Content(persistentListOf()),
|
||||
bottomSheetConfig = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val params: NFTReceiveComponent.Params = paramsContainer.require()
|
||||
|
||||
init {
|
||||
subscribeToNFTAvailableNetworks()
|
||||
}
|
||||
|
||||
private fun subscribeToNFTAvailableNetworks() {
|
||||
combine(
|
||||
flow = getNFTAvailableNetworksUseCase(params.userWalletId),
|
||||
flow2 = searchManager.query.distinctUntilChanged(),
|
||||
) { networks, query ->
|
||||
filterNFTAvailableNetworksUseCase(networks, query)
|
||||
}
|
||||
.onEach { filteredNetworks ->
|
||||
_state.update {
|
||||
UpdateDataStateTransformer(
|
||||
networks = filteredNetworks,
|
||||
onNetworkClick = ::onNetworkClick,
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onSearchQueryChange(newQuery: String) {
|
||||
modelScope.launch {
|
||||
_state.update { UpdateSearchQueryTransformer(newQuery).transform(it) }
|
||||
|
||||
searchManager.update(newQuery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialSearchBar(): SearchBarUM = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.common_search),
|
||||
query = "",
|
||||
isActive = false,
|
||||
onQueryChange = ::onSearchQueryChange,
|
||||
onActiveChange = ::toggleSearchBar,
|
||||
)
|
||||
|
||||
private fun toggleSearchBar(isActive: Boolean) {
|
||||
_state.update {
|
||||
ToggleSearchBarTransformer(isActive).transform(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReceiveBottomSheetDismiss() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNetworkClick(network: Network) {
|
||||
modelScope.launch {
|
||||
val networkStatus = getNFTNetworkStatusUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
network = network,
|
||||
) ?: return@launch
|
||||
|
||||
when (val value = networkStatus.value) {
|
||||
is NetworkStatus.Verified -> {
|
||||
_state.update {
|
||||
ShowReceiveBottomSheetTransformer(
|
||||
network = network,
|
||||
networkAddress = value.address,
|
||||
onDismissBottomSheet = ::onReceiveBottomSheetDismiss,
|
||||
onCopyClick = ::onCopyClick,
|
||||
onShareClick = ::onShareClick,
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
is NetworkStatus.MissedDerivation,
|
||||
is NetworkStatus.NoAccount,
|
||||
is NetworkStatus.Unreachable,
|
||||
-> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCopyClick(text: String) {
|
||||
clipboardManager.setText(text = text, isSensitive = true)
|
||||
}
|
||||
|
||||
private fun onShareClick(text: String) {
|
||||
shareManager.shareText(text = text)
|
||||
}
|
||||
|
||||
private fun navigateBack() {
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.features.nft.receive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
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.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 com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.features.nft.receive.entity.NFTNetworkUM
|
||||
|
||||
@Composable
|
||||
internal fun NFTNetwork(state: NFTNetworkUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { state.onItemClick() }
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size24)
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(state.iconRes),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),
|
||||
text = state.name,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTNetwork(@PreviewParameter(NFTNetworkProvider::class) state: NFTNetworkUM) {
|
||||
TangemThemePreview {
|
||||
NFTNetwork(
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class NFTNetworkProvider : CollectionPreviewParameterProvider<NFTNetworkUM>(
|
||||
collection = listOf(
|
||||
NFTNetworkUM(
|
||||
id = "item1",
|
||||
name = "Nethers #0853",
|
||||
iconRes = R.drawable.img_eth_22,
|
||||
onItemClick = { },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.features.nft.receive.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
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.components.fields.SearchBar
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
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.receive.entity.NFTReceiveUM
|
||||
|
||||
@Composable
|
||||
internal fun NFTReceive(state: NFTReceiveUM, 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_close_24,
|
||||
onIconClicked = state.onBackClick,
|
||||
),
|
||||
title = stringResourceSafe(id = R.string.nft_receive_title),
|
||||
subtitle = stringResourceSafe(id = R.string.nft_receive_subtitle),
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
SearchBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
state = state.search,
|
||||
colors = TangemSearchBarDefaults.secondaryTextFieldColors,
|
||||
)
|
||||
|
||||
val contentModifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing16)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = state.networks,
|
||||
contentKey = { it::class },
|
||||
label = "NFT Receive",
|
||||
) {
|
||||
when (val networks = it) {
|
||||
is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks, contentModifier)
|
||||
is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty(contentModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
ShowBottomSheet(state.bottomSheetConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
|
||||
if (bottomSheetConfig != null) {
|
||||
when (bottomSheetConfig.content) {
|
||||
is TokenReceiveBottomSheetConfig -> {
|
||||
TokenReceiveBottomSheet(config = bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.features.nft.receive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.features.nft.receive.entity.NFTNetworkUM
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
internal fun NFTReceiveNetworksContent(state: NFTReceiveUM.Networks.Content, modifier: Modifier = Modifier) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.animateContentSize(),
|
||||
state = listState,
|
||||
) {
|
||||
item(
|
||||
key = "title",
|
||||
) {
|
||||
Title()
|
||||
}
|
||||
items(
|
||||
items = state.items,
|
||||
key = NFTNetworkUM::id,
|
||||
) { network ->
|
||||
NFTNetwork(
|
||||
state = network,
|
||||
modifier = Modifier.animateItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing4,
|
||||
),
|
||||
text = stringResourceSafe(R.string.nft_receive_choose_network),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Preview(widthDp = 360, showBackground = true)
|
||||
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NFTReceiveNetworksUM() {
|
||||
TangemThemePreview {
|
||||
NFTReceiveNetworksContent(
|
||||
state = NFTReceiveUM.Networks.Content(
|
||||
items = persistentListOf(
|
||||
NFTNetworkUM(
|
||||
id = "item1",
|
||||
name = "Ethereum",
|
||||
iconRes = R.drawable.img_eth_22,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTNetworkUM(
|
||||
id = "item2",
|
||||
name = "Polygon",
|
||||
iconRes = R.drawable.img_polygon_22,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTNetworkUM(
|
||||
id = "item3",
|
||||
name = "BSC",
|
||||
iconRes = R.drawable.img_bsc_22,
|
||||
onItemClick = { },
|
||||
),
|
||||
NFTNetworkUM(
|
||||
id = "item4",
|
||||
name = "Avalanche",
|
||||
iconRes = R.drawable.img_avalanche_22,
|
||||
onItemClick = { },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.nft.receive.ui
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun NFTReceiveNetworksEmpty(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.nft_empty_search),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
|
@ -4,4 +4,5 @@ interface OnboardingV2FeatureToggles {
|
|||
val isOnboardingV2Enabled: Boolean
|
||||
val isVisaOnboardingEnabled: Boolean
|
||||
val isNoteRefactoringEnabled: Boolean
|
||||
val isTwinRefactoringEnabled: Boolean
|
||||
}
|
||||
|
|
@ -8,12 +8,14 @@ interface OnboardingEntryComponent : ComposableContentComponent {
|
|||
|
||||
data class Params(
|
||||
val scanResponse: ScanResponse,
|
||||
val multiWalletMode: MultiWalletMode,
|
||||
val mode: Mode,
|
||||
)
|
||||
|
||||
enum class MultiWalletMode {
|
||||
enum class Mode {
|
||||
Onboarding,
|
||||
AddBackup,
|
||||
AddBackupWallet1,
|
||||
WelcomeOnlyTwin,
|
||||
RecreateWalletTwin,
|
||||
ContinueFinalize,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.error.ext)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
@ -43,6 +44,9 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.onboarding)
|
||||
implementation(projects.domain.visa)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.onramp)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
|
@ -63,6 +67,7 @@ dependencies {
|
|||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.lottie.compose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
@ -73,6 +78,7 @@ dependencies {
|
|||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
|
|
|
|||
|
|
@ -12,4 +12,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor(
|
|||
get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED")
|
||||
override val isNoteRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED")
|
||||
override val isTwinRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.analytics
|
||||
package com.tangem.features.onboarding.v2.common.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
sealed class OnboardingEvent(
|
||||
category: String,
|
||||
|
|
@ -38,6 +40,21 @@ sealed class OnboardingEvent(
|
|||
}
|
||||
}
|
||||
|
||||
sealed class Topup(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingEvent("Onboarding / Top Up", event, params) {
|
||||
|
||||
object ScreenOpened : Topup("Activation Screen Opened")
|
||||
|
||||
object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")
|
||||
|
||||
class ButtonBuyCrypto(currency: CryptoCurrency) : Topup(
|
||||
event = "Button - Buy Crypto",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.symbol),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Backup(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
|
|
@ -75,4 +92,22 @@ sealed class OnboardingEvent(
|
|||
params = mapOf("Action" to "Cancel"),
|
||||
)
|
||||
}
|
||||
|
||||
sealed class Twins(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : OnboardingEvent("Onboarding / Twins", event, params) {
|
||||
|
||||
data object ScreenOpened : Twins("Twinning Screen Opened")
|
||||
data object SetupStarted : Twins("Twin Setup Started")
|
||||
data object SetupFinished : Twins("Twin Setup Finished")
|
||||
}
|
||||
|
||||
data class OfflineAttestationFailed(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
) : OnboardingEvent(
|
||||
category = "Error",
|
||||
event = "Offline Attestation Failed",
|
||||
params = mapOf(AnalyticsParam.SOURCE to source.value),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(R.string.onboarding_exit_alert_title),
|
||||
message = resourceReference(R.string.onboarding_exit_alert_message),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_yes),
|
||||
onClick = onConfirm,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal fun interruptBackupDialog(onConfirm: () -> Unit) = DialogMessage(
|
||||
title = resourceReference(R.string.onboarding_exit_alert_title),
|
||||
message = resourceReference(R.string.onboarding_exit_alert_message),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_ok),
|
||||
warning = true,
|
||||
onClick = onConfirm,
|
||||
),
|
||||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
|
||||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.onboarding.v2.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size48)
|
||||
.shadow(elevation = 2.dp, shape = CircleShape)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(
|
||||
enabled = !isRefreshing,
|
||||
onClick = onRefreshBalanceClick,
|
||||
indication = ripple(),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
),
|
||||
) {
|
||||
if (isRefreshing) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_refresh_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,14 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
|
||||
interface OnboardingDoneComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val onDone: () -> Unit)
|
||||
data class Params(
|
||||
val mode: Mode,
|
||||
val onDone: () -> Unit,
|
||||
)
|
||||
|
||||
enum class Mode {
|
||||
WalletCreated, GoodToGo
|
||||
}
|
||||
|
||||
interface Factory : ComponentFactory<Params, OnboardingDoneComponent>
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.done.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
|
|
@ -18,7 +17,8 @@ internal class DefaultOnboardingDoneComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
OnboardingDone(
|
||||
onContinueClick = remember(this) { params.onDone },
|
||||
mode = params.mode,
|
||||
onContinueClick = params.onDone,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,16 @@ import com.tangem.core.ui.components.*
|
|||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
internal fun OnboardingDone(
|
||||
mode: OnboardingDoneComponent.Mode,
|
||||
onContinueClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
|
||||
val progress by animateLottieCompositionAsState(composition)
|
||||
var showConfetti by remember { mutableStateOf(false) }
|
||||
|
|
@ -57,7 +63,12 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo
|
|||
SpacerH12()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
text = stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding),
|
||||
text = when (mode) {
|
||||
OnboardingDoneComponent.Mode.WalletCreated ->
|
||||
stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
|
||||
OnboardingDoneComponent.Mode.GoodToGo ->
|
||||
stringResourceSafe(R.string.visa_onboarding_success_screen_description)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
|
|
@ -100,6 +111,9 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo
|
|||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
OnboardingDone(onContinueClick = {})
|
||||
OnboardingDone(
|
||||
mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
onContinueClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor(
|
|||
route = configuration,
|
||||
childContext = childByContext(factoryContext),
|
||||
onManageTokensDone = model::onManageTokensDone,
|
||||
model = model,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,15 +15,20 @@ 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
|
||||
import com.tangem.domain.wallets.legacy.asLockable
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.biometry.BiometryFeatureToggles
|
||||
import com.tangem.features.onboarding.v2.TitleProvider
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
|
||||
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode
|
||||
import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent
|
||||
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.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
|
|
@ -43,6 +48,7 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
private val askBiometryFeatureToggles: BiometryFeatureToggles,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
|
||||
|
|
@ -56,6 +62,7 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
}
|
||||
|
||||
val startRoute = routeByProductType(params.scanResponse)
|
||||
val onboardingTwinModelCallbacks = OnboardingTwinModelCallbacks()
|
||||
|
||||
fun onManageTokensDone() {
|
||||
navigateToFinalScreenFlow()
|
||||
|
|
@ -66,32 +73,26 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute {
|
||||
val multiWalletNavigationMode = when (params.multiWalletMode) {
|
||||
OnboardingEntryComponent.MultiWalletMode.Onboarding ->
|
||||
OnboardingMultiWalletComponent.Mode.Onboarding
|
||||
OnboardingEntryComponent.MultiWalletMode.AddBackup ->
|
||||
OnboardingMultiWalletComponent.Mode.AddBackup
|
||||
OnboardingEntryComponent.MultiWalletMode.ContinueFinalize ->
|
||||
OnboardingMultiWalletComponent.Mode.ContinueFinalize
|
||||
}
|
||||
|
||||
return when (scanResponse.productType) {
|
||||
ProductType.Wallet -> OnboardingRoute.MultiWallet(
|
||||
scanResponse = scanResponse,
|
||||
withSeedPhraseFlow = false,
|
||||
titleProvider = titleProvider,
|
||||
onDone = ::onMultiWalletOnboardingDone,
|
||||
mode = multiWalletNavigationMode,
|
||||
)
|
||||
ProductType.Ring,
|
||||
ProductType.Wallet,
|
||||
ProductType.Wallet2,
|
||||
-> OnboardingRoute.MultiWallet(
|
||||
scanResponse = scanResponse,
|
||||
withSeedPhraseFlow = true,
|
||||
titleProvider = titleProvider,
|
||||
onDone = ::onMultiWalletOnboardingDone,
|
||||
mode = multiWalletNavigationMode,
|
||||
)
|
||||
ProductType.Ring,
|
||||
-> {
|
||||
val multiWalletNavigationMode = when (params.mode) {
|
||||
Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding
|
||||
Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup
|
||||
Mode.ContinueFinalize -> OnboardingMultiWalletComponent.Mode.ContinueFinalize
|
||||
else -> error("Incorrect onboarding type")
|
||||
}
|
||||
|
||||
OnboardingRoute.MultiWallet(
|
||||
scanResponse = scanResponse,
|
||||
withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet,
|
||||
titleProvider = titleProvider,
|
||||
onDone = ::onMultiWalletOnboardingDone,
|
||||
mode = multiWalletNavigationMode,
|
||||
)
|
||||
}
|
||||
ProductType.Visa -> OnboardingRoute.Visa(
|
||||
scanResponse = scanResponse,
|
||||
titleProvider = titleProvider,
|
||||
|
|
@ -102,14 +103,33 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
titleProvider = titleProvider,
|
||||
onDone = ::navigateToFinalScreenFlow,
|
||||
)
|
||||
ProductType.Twins -> {
|
||||
val mode = when (params.mode) {
|
||||
Mode.Onboarding -> OnboardingTwinComponent.Params.Mode.CreateWallet
|
||||
Mode.WelcomeOnlyTwin -> OnboardingTwinComponent.Params.Mode.WelcomeOnly
|
||||
Mode.RecreateWalletTwin -> OnboardingTwinComponent.Params.Mode.RecreateWallet
|
||||
else -> error("Incorrect onboarding type")
|
||||
}
|
||||
|
||||
OnboardingRoute.Twins(
|
||||
scanResponse = scanResponse,
|
||||
titleProvider = titleProvider,
|
||||
mode = mode,
|
||||
)
|
||||
}
|
||||
else -> error("Unsupported")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMultiWalletOnboardingDone(userWallet: UserWallet) {
|
||||
when {
|
||||
params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
params.mode == Mode.AddBackupWallet1 -> {
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
onDone = ::exitComponentScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed() -> {
|
||||
stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet))
|
||||
|
|
@ -121,48 +141,104 @@ internal class OnboardingEntryModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onVisaOnboardingDone() {
|
||||
navigateToFinalScreenFlow()
|
||||
navigateToFinalScreenFlow(doneMode = OnboardingDoneComponent.Mode.GoodToGo)
|
||||
}
|
||||
|
||||
private fun navigateToFinalScreenFlow() {
|
||||
private fun navigateToFinalScreenFlow(
|
||||
doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
) {
|
||||
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
|
||||
modelScope.launch {
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()),
|
||||
OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)),
|
||||
)
|
||||
} else {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::exitComponentScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::exitComponentScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
|
||||
inner class AskBiometryModelCallbacks(
|
||||
private val doneMode: OnboardingDoneComponent.Mode,
|
||||
) : AskBiometryComponent.ModelCallbacks {
|
||||
override fun onAllowed() {
|
||||
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On))
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::exitComponentScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDenied() {
|
||||
analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off))
|
||||
stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen))
|
||||
stackNavigation.replaceAll(
|
||||
OnboardingRoute.Done(
|
||||
mode = doneMode,
|
||||
onDone = ::exitComponentScreen,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToWalletScreen() {
|
||||
private fun exitComponentScreen() {
|
||||
if (askBiometryFeatureToggles.isAskForBiometryEnabled) {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
} else {
|
||||
modelScope.launch(NonCancellable) {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
delay(timeMillis = 1_800)
|
||||
router.push(AppRoute.SaveWallet)
|
||||
if (userWalletsListManager.hasUserWallets) {
|
||||
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
|
||||
|
||||
if (isLocked) {
|
||||
router.replaceAll(AppRoute.Welcome())
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Home)
|
||||
}
|
||||
} else {
|
||||
if (userWalletsListManager.hasUserWallets) {
|
||||
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
|
||||
|
||||
if (isLocked) {
|
||||
router.replaceAll(AppRoute.Welcome())
|
||||
} else {
|
||||
modelScope.launch(NonCancellable) {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
if (tangemSdkManager.checkCanUseBiometry() &&
|
||||
settingsRepository.shouldShowSaveUserWalletScreen()
|
||||
) {
|
||||
delay(timeMillis = 1_800)
|
||||
router.push(AppRoute.SaveWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
router.replaceAll(AppRoute.Home)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks {
|
||||
override fun onDone() {
|
||||
navigateToFinalScreenFlow()
|
||||
}
|
||||
|
||||
override fun onBack() {
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.managetokens.component.OnboardingManageTokensComponent
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class OnboardingChildFactory @Inject constructor(
|
||||
private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory,
|
||||
private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory,
|
||||
|
|
@ -16,9 +19,15 @@ internal class OnboardingChildFactory @Inject constructor(
|
|||
private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory,
|
||||
private val onboardingTwinComponentFactory: OnboardingTwinComponent.Factory,
|
||||
) {
|
||||
|
||||
fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any {
|
||||
fun createChild(
|
||||
route: OnboardingRoute,
|
||||
childContext: AppComponentContext,
|
||||
model: OnboardingEntryModel,
|
||||
onManageTokensDone: () -> Unit,
|
||||
): Any {
|
||||
return when (route) {
|
||||
is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create(
|
||||
context = childContext,
|
||||
|
|
@ -48,6 +57,7 @@ internal class OnboardingChildFactory @Inject constructor(
|
|||
is OnboardingRoute.Done -> onboardingDoneComponentFactory.create(
|
||||
context = childContext,
|
||||
params = OnboardingDoneComponent.Params(
|
||||
mode = route.mode,
|
||||
onDone = route.onDone,
|
||||
),
|
||||
)
|
||||
|
|
@ -59,6 +69,15 @@ internal class OnboardingChildFactory @Inject constructor(
|
|||
onDone = route.onDone,
|
||||
),
|
||||
)
|
||||
is OnboardingRoute.Twins -> onboardingTwinComponentFactory.create(
|
||||
context = childContext,
|
||||
params = OnboardingTwinComponent.Params(
|
||||
scanResponse = route.scanResponse,
|
||||
modelCallbacks = model.onboardingTwinModelCallbacks,
|
||||
titleProvider = route.titleProvider,
|
||||
mode = route.mode,
|
||||
),
|
||||
)
|
||||
is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AskBiometryComponent.Params(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.onboarding.v2.TitleProvider
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent
|
||||
|
||||
sealed class OnboardingRoute : Route {
|
||||
|
||||
|
|
@ -31,6 +33,12 @@ sealed class OnboardingRoute : Route {
|
|||
val onDone: () -> Unit,
|
||||
) : OnboardingRoute()
|
||||
|
||||
data class Twins(
|
||||
val titleProvider: TitleProvider,
|
||||
val scanResponse: ScanResponse,
|
||||
val mode: OnboardingTwinComponent.Params.Mode,
|
||||
) : OnboardingRoute()
|
||||
|
||||
data class ManageTokens(
|
||||
val userWallet: UserWallet,
|
||||
) : OnboardingRoute()
|
||||
|
|
@ -40,6 +48,7 @@ sealed class OnboardingRoute : Route {
|
|||
) : OnboardingRoute()
|
||||
|
||||
data class Done(
|
||||
val mode: OnboardingDoneComponent.Mode,
|
||||
val onDone: () -> Unit,
|
||||
) : OnboardingRoute()
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation
|
|||
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -36,7 +36,7 @@ private fun Content(state: MultiWalletAccessCodeUM, modifier: Modifier = Modifie
|
|||
modifier = Modifier.weight(1f),
|
||||
targetState = state.step,
|
||||
transitionSpec = TangemAnimations.AnimatedContent
|
||||
.slide { initial, target -> initial.ordinal > target.ordinal },
|
||||
.slide { initial, target -> target.ordinal > initial.ordinal },
|
||||
label = "AnimatedContent",
|
||||
) { step ->
|
||||
when (step) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) {
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_access_code),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_access_code_119),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -19,8 +20,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog
|
||||
|
|
@ -48,6 +49,7 @@ class MultiWalletBackupModel @Inject constructor(
|
|||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
|
|
@ -241,7 +243,7 @@ class MultiWalletBackupModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
else -> FirebaseCrashlytics.getInstance().recordException(result.error)
|
||||
else -> analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -25,7 +24,7 @@ import javax.inject.Inject
|
|||
internal class Wallet1ChooseOptionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
|
|
@ -67,7 +66,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(),
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
|
||||
|
|
@ -43,7 +42,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -146,7 +145,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(),
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class MultiWalletCreateWalletUM(
|
||||
val title: TextReference,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
|
|
@ -49,7 +48,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
|
|
@ -281,7 +280,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, generateWalletNameUseCase)
|
||||
value = userWalletBuilderFactory.create(scanResponse = scanResponse)
|
||||
.backupCardsIds(backupCardIds.toSet())
|
||||
.hasBackupError(walletHasBackupError)
|
||||
.build(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state
|
||||
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class MultiWalletFinalizeUM(
|
||||
val step: Step = Step.Primary,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,13 @@ import com.tangem.domain.card.repository.CardRepository
|
|||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Mod
|
|||
AnimatedContent(
|
||||
modifier = modifier.navigationBarsPadding(),
|
||||
targetState = state,
|
||||
transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order < from.order },
|
||||
transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order > from.order },
|
||||
contentKey = { st -> st::class.java },
|
||||
label = "animatedContent",
|
||||
) { st ->
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
||||
internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) =
|
||||
|
|
@ -19,20 +20,4 @@ internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismi
|
|||
onDismissButtonClick()
|
||||
dismiss()
|
||||
},
|
||||
)
|
||||
|
||||
internal fun interruptBackupDialog(onConfirm: () -> Unit, dismiss: () -> Unit) = OnboardingDialogUM(
|
||||
title = resourceReference(R.string.onboarding_exit_alert_title),
|
||||
message = resourceReference(R.string.onboarding_exit_alert_message),
|
||||
confirmButtonText = resourceReference(R.string.common_ok),
|
||||
dismissButtonText = resourceReference(R.string.common_cancel),
|
||||
dismissWarningColor = true,
|
||||
onDismiss = dismiss,
|
||||
onConfirmClick = {
|
||||
onConfirm()
|
||||
dismiss()
|
||||
},
|
||||
onDismissButtonClick = {
|
||||
dismiss()
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
@ -5,34 +5,40 @@ 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.decompose.ui.UiMessageSender
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
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.common.ui.interruptBackupDialog
|
||||
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.backup.BackupService
|
||||
import com.tangem.sdk.api.BackupServiceHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class OnboardingMultiWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
analyticsHandler: AnalyticsEventHandler,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val backupServiceHolder: BackupServiceHolder,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val getCardImageUseCase: GetCardImageUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
|
||||
private val getCardImageUseCase = GetCardImageUseCase()
|
||||
private val _uiState = MutableStateFlow(OnboardingMultiWalletUM())
|
||||
|
||||
val state = MutableStateFlow(
|
||||
|
|
@ -56,19 +62,16 @@ internal class OnboardingMultiWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onBack() {
|
||||
_uiState.update { st ->
|
||||
st.copy(
|
||||
dialog = interruptBackupDialog(
|
||||
onConfirm = {
|
||||
modelScope.launch {
|
||||
onboardingRepository.clearUnfinishedFinalizeOnboarding()
|
||||
router.pop()
|
||||
}
|
||||
},
|
||||
dismiss = { _uiState.update { it.copy(dialog = null) } },
|
||||
),
|
||||
)
|
||||
}
|
||||
uiMessageSender.send(
|
||||
interruptBackupDialog(
|
||||
onConfirm = {
|
||||
modelScope.launch {
|
||||
onboardingRepository.clearUnfinishedFinalizeOnboarding()
|
||||
router.pop()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeToBackups() {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
|
||||
|
||||
|
|
@ -23,23 +20,6 @@ internal fun OnboardingMultiWallet(
|
|||
modifier: Modifier = Modifier,
|
||||
childContent: @Composable (Modifier) -> Unit = {},
|
||||
) {
|
||||
if (state.dialog != null) {
|
||||
BasicDialog(
|
||||
title = state.dialog.title.resolveReference(),
|
||||
message = state.dialog.message.resolveReference(),
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.dialog.confirmButtonText.resolveReference(),
|
||||
onClick = state.dialog.onConfirmClick,
|
||||
),
|
||||
dismissButton = DialogButtonUM(
|
||||
title = state.dialog.dismissButtonText.resolveReference(),
|
||||
warning = state.dialog.dismissWarningColor,
|
||||
onClick = state.dialog.onDismissButtonClick,
|
||||
),
|
||||
onDismissDialog = state.dialog.onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state
|
||||
|
||||
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM
|
||||
|
||||
internal data class OnboardingMultiWalletUM(
|
||||
val artwork1Url: String? = null,
|
||||
val artwork2Url: String? = null,
|
||||
val artwork3Url: String? = null,
|
||||
val dialog: OnboardingDialogUM? = null,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue