Updated on 2026-08-14
This commit is contained in:
commit
e1b8a026cb
79 changed files with 2712 additions and 43 deletions
|
|
@ -16,6 +16,7 @@ dependencies {
|
|||
api(projects.features.markets.api)
|
||||
api(projects.features.onramp.api)
|
||||
api(projects.features.sendV2.api)
|
||||
api(projects.features.tokenRecieve.api)
|
||||
|
||||
/* Data */
|
||||
implementation(projects.data.common)
|
||||
|
|
@ -39,6 +40,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
|
|||
|
|
@ -5,12 +5,20 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel
|
||||
import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -19,20 +27,41 @@ import dagger.assisted.AssistedInject
|
|||
internal class DefaultMarketsPortfolioComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: MarketsPortfolioComponent.Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : AppComponentContext by context, MarketsPortfolioComponent {
|
||||
|
||||
private val model: MarketsPortfolioModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
override fun setTokenNetworks(networks: List<TokenMarketInfo.Network>) = model.setTokenNetworks(networks)
|
||||
override fun setNoNetworksAvailable() = model.setNoNetworksAvailable()
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
MyPortfolio(modifier = modifier, state = state)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MarketsPortfolioComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.features.markets.portfolio.impl.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -15,24 +17,31 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase
|
||||
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
|
||||
import com.tangem.domain.markets.SaveMarketTokensUseCase
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.ArtworkModel
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
|
||||
import com.tangem.features.markets.impl.R
|
||||
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
|
||||
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
|
||||
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
|
||||
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -43,7 +52,7 @@ import kotlinx.coroutines.sync.withLock
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class MarketsPortfolioModel @Inject constructor(
|
||||
|
|
@ -60,6 +69,9 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
private val getCardImageUseCase: GetCardImageUseCase,
|
||||
private val addToPortfolioManager: AddToPortfolioManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : Model() {
|
||||
|
||||
val state: StateFlow<MyPortfolioUM> get() = _state
|
||||
|
|
@ -80,6 +92,8 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
|
||||
private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel())
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
|
||||
.map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
|
|
@ -128,7 +142,11 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
tokenActionsHandler = tokenActionsIntentsFactory.create(
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
updateTokenReceiveBSConfig = { updateBlock ->
|
||||
updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) }
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled.not()) {
|
||||
updateTokensState {
|
||||
it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig))
|
||||
}
|
||||
}
|
||||
},
|
||||
onHandleQuickAction = { handledAction ->
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -137,6 +155,9 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
|
||||
),
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
configureReceiveAddresses(handledAction)
|
||||
}
|
||||
},
|
||||
),
|
||||
updateTokens = { updateBlock ->
|
||||
|
|
@ -359,4 +380,51 @@ internal class MarketsPortfolioModel @Inject constructor(
|
|||
block(tokensState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) {
|
||||
when (quickAction.action) {
|
||||
TokenActionsBSContentUM.Action.Receive -> {
|
||||
val addresses = quickAction.cryptoCurrencyData.status.value.networkAddress ?: return
|
||||
val cryptoCurrency = quickAction.cryptoCurrencyData.status.currency
|
||||
modelScope.launch {
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val tokenConfig = TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId,
|
||||
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
bottomSheetNavigation.activate(tokenConfig)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.nft.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -34,6 +35,8 @@ dependencies {
|
|||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.tokens)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.features.nft.details.info.NFTDetailsInfoComponent
|
|||
import com.tangem.features.nft.entity.NFTSendSuccessListener
|
||||
import com.tangem.features.nft.receive.NFTReceiveComponent
|
||||
import com.tangem.features.nft.traits.NFTAssetTraitsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -35,6 +36,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
@Assisted private val params: NFTComponent.Params,
|
||||
private val nftDetailsInfoComponentFactory: NFTDetailsInfoComponent.Factory,
|
||||
nftSendSuccessListener: NFTSendSuccessListener,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : NFTComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<NFTRoute>()
|
||||
|
|
@ -134,6 +136,7 @@ internal class DefaultNFTComponent @AssistedInject constructor(
|
|||
walletName = params.walletName,
|
||||
onBackClick = ::onChildBack,
|
||||
),
|
||||
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
|
||||
)
|
||||
|
||||
private fun getDetailsComponent(
|
||||
|
|
|
|||
|
|
@ -4,29 +4,58 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.nft.receive.model.NFTReceiveModel
|
||||
import com.tangem.features.nft.receive.ui.NFTReceive
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class NFTReceiveComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val model: NFTReceiveModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
NFTReceive(state, modifier)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val walletName: String,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.nft.receive.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -12,12 +14,20 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.Asset
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.TokenReceiveNotification
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.network.NetworkStatus
|
||||
import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase
|
||||
import com.tangem.domain.nft.GetNFTCurrencyUseCase
|
||||
import com.tangem.domain.nft.GetNFTNetworkStatusUseCase
|
||||
import com.tangem.domain.nft.GetNFTNetworksUseCase
|
||||
import com.tangem.domain.nft.analytics.NFTAnalyticsEvent
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.features.nft.impl.R
|
||||
import com.tangem.features.nft.receive.NFTReceiveComponent
|
||||
import com.tangem.features.nft.receive.entity.NFTReceiveUM
|
||||
|
|
@ -25,6 +35,8 @@ import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheet
|
|||
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.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -43,11 +55,17 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
private val shareManager: ShareManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
private val getNFTCurrencyUseCase: GetNFTCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params: NFTReceiveComponent.Params = paramsContainer.require()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val _state = MutableStateFlow(
|
||||
value = NFTReceiveUM(
|
||||
onBackClick = params.onBackClick,
|
||||
|
|
@ -138,14 +156,23 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
|
||||
when (val value = networkStatus.value) {
|
||||
is NetworkStatus.Verified -> {
|
||||
_state.update {
|
||||
ShowReceiveBottomSheetTransformer(
|
||||
network = network,
|
||||
networkAddress = value.address,
|
||||
onDismissBottomSheet = ::onReceiveBottomSheetDismiss,
|
||||
onCopyClick = { text -> onCopyClick(text, network) },
|
||||
onShareClick = { text -> onShareClick(text, network) },
|
||||
).transform(it)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
bottomSheetNavigation.activate(
|
||||
configuration = configureReceiveAddresses(
|
||||
addresses = value.address,
|
||||
network = network,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
_state.update {
|
||||
ShowReceiveBottomSheetTransformer(
|
||||
network = network,
|
||||
networkAddress = value.address,
|
||||
onDismissBottomSheet = ::onReceiveBottomSheetDismiss,
|
||||
onCopyClick = { text -> onCopyClick(text, network) },
|
||||
onShareClick = { text -> onShareClick(text, network) },
|
||||
).transform(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkStatus.MissedDerivation,
|
||||
|
|
@ -166,4 +193,56 @@ internal class NFTReceiveModel @Inject constructor(
|
|||
analyticsEventHandler.send(NFTAnalyticsEvent.Receive.ShareAddress(network.name))
|
||||
shareManager.shareText(text = text)
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(addresses: NetworkAddress, network: Network): TokenReceiveConfig {
|
||||
val cryptoCurrency = getNFTCurrencyUseCase.invoke(network)
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = params.userWalletId,
|
||||
network = network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = cryptoCurrency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val notifications = buildList {
|
||||
if (BlockchainUtils.isSolana(network.rawId)) {
|
||||
add(
|
||||
TokenReceiveNotification(
|
||||
title = R.string.nft_receive_unsupported_types,
|
||||
subtitle = R.string.nft_receive_unsupported_types_description,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = Asset.NFT.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = params.userWalletId,
|
||||
showMemoDisclaimer = false,
|
||||
receiveAddress = receiveAddresses,
|
||||
tokenReceiveNotification = notifications,
|
||||
asset = Asset.NFT,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ dependencies {
|
|||
implementation(projects.features.manageTokens.api)
|
||||
implementation(projects.features.biometry.api)
|
||||
implementation(projects.features.hotWallet.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
@ -49,6 +50,7 @@ dependencies {
|
|||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(projects.libs.tangemSdkApi)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
|||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
|
||||
import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute
|
||||
import dagger.assisted.Assisted
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.features.onboarding.v2.twin.impl
|
|||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
|
@ -42,7 +42,6 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler { model.onBack() }
|
||||
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
OnboardingTwin(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
|
@ -23,6 +24,7 @@ internal sealed class SendAnalyticEvents(
|
|||
val feeType: AnalyticsParam.FeeType,
|
||||
val blockchain: String,
|
||||
val nonceNotEmpty: Boolean,
|
||||
private val ensStatus: AnalyticsParam.EnsStatus,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Transaction Sent Screen Opened",
|
||||
params = mapOf(
|
||||
|
|
@ -30,6 +32,10 @@ internal sealed class SendAnalyticEvents(
|
|||
FEE_TYPE to feeType.value,
|
||||
BLOCKCHAIN to blockchain,
|
||||
NONCE to nonceNotEmpty.toString().capitalize(),
|
||||
ENS_ADDRESS to when (ensStatus) {
|
||||
AnalyticsParam.EnsStatus.EMPTY -> false.toString()
|
||||
AnalyticsParam.EnsStatus.FULL -> true.toString()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal class SendAnalyticHelper @Inject constructor(
|
|||
feeType = feeType,
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
nonceNotEmpty = feeSelectorUM.nonce != null,
|
||||
ensStatus = getEnsStatus(sendUM),
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
|
|
@ -52,4 +53,14 @@ internal class SendAnalyticHelper @Inject constructor(
|
|||
else -> Basic.TransactionSent.MemoType.Null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEnsStatus(sendUM: SendUM): AnalyticsParam.EnsStatus {
|
||||
val blockchainAddressForEns =
|
||||
(sendUM.destinationUM as? DestinationUM.Content)?.addressTextField?.blockchainAddress
|
||||
return if (blockchainAddressForEns != null) {
|
||||
AnalyticsParam.EnsStatus.FULL
|
||||
} else {
|
||||
AnalyticsParam.EnsStatus.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
1
features/token-recieve/api/.gitignore
vendored
Normal file
1
features/token-recieve/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
features/token-recieve/api/build.gradle.kts
Normal file
23
features/token-recieve/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("kotlin-parcelize")
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.tokenrecieve.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.tokenreceive
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
|
||||
interface TokenReceiveComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val config: TokenReceiveConfig,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TokenReceiveComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): TokenReceiveComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.tokenreceive
|
||||
|
||||
interface TokenReceiveFeatureToggle {
|
||||
|
||||
val isNewTokenReceiveEnabled: Boolean
|
||||
}
|
||||
1
features/token-recieve/impl/.gitignore
vendored
Normal file
1
features/token-recieve/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
58
features/token-recieve/impl/build.gradle.kts
Normal file
58
features/token-recieve/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.tokenreceive.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.ui.utils)
|
||||
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.configToggles)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.tokenreceive
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultTokenReceiveFeatureToggle(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : TokenReceiveFeatureToggle {
|
||||
|
||||
override val isNewTokenReceiveEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled("NEW_TOKEN_RECEIVE_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.features.tokenreceive.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveModel
|
||||
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
|
||||
import com.tangem.features.tokenreceive.ui.TokenReceiveContentSheet
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultTokenReceiveComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TokenReceiveComponent.Params,
|
||||
) : TokenReceiveComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TokenReceiveModel = getOrCreateModel(params)
|
||||
private val innerRouter = InnerRouter<TokenReceiveRoutes>(
|
||||
stackNavigation = model.stackNavigation,
|
||||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val contentStack = childStack(
|
||||
key = "TokenReceiveFlowStack",
|
||||
source = model.stackNavigation,
|
||||
serializer = TokenReceiveRoutes.serializer(),
|
||||
initialConfiguration = getInitialConfig(model.params.config.shouldShowWarning),
|
||||
handleBackButton = false,
|
||||
childFactory = ::screenChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val content by contentStack.subscribeAsState()
|
||||
val currentRoute = content.active.configuration
|
||||
|
||||
TokenReceiveContentSheet(
|
||||
route = currentRoute,
|
||||
onCloseClick = ::dismiss,
|
||||
onBackClick = ::onChildBack,
|
||||
contentStack = content,
|
||||
)
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
model.params.onDismiss()
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
when (contentStack.value.active.configuration) {
|
||||
is TokenReceiveRoutes.QrCode -> model.stackNavigation.pop()
|
||||
TokenReceiveRoutes.ReceiveAssets,
|
||||
TokenReceiveRoutes.Warning,
|
||||
-> dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun screenChild(
|
||||
config: TokenReceiveRoutes,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableContentComponent {
|
||||
val appComponentContext = childByContext(
|
||||
componentContext = componentContext,
|
||||
router = innerRouter,
|
||||
)
|
||||
return when (config) {
|
||||
is TokenReceiveRoutes.QrCode -> TokenReceiveQrCodeComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams(
|
||||
network = model.params.config.cryptoCurrency.network.name,
|
||||
address = model.state.value.addresses[config.addressId] ?: error("Address has to be there"),
|
||||
callback = model,
|
||||
onDismiss = ::dismiss,
|
||||
id = config.addressId,
|
||||
),
|
||||
)
|
||||
TokenReceiveRoutes.ReceiveAssets -> TokenReceiveAssetsComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = TokenReceiveAssetsComponent.TokenReceiveAssetsParams(
|
||||
addresses = model.state.value.addresses,
|
||||
callback = model,
|
||||
onDismiss = ::dismiss,
|
||||
notificationConfigs = model.state.value.notificationConfigs,
|
||||
showMemoDisclaimer = model.params.config.showMemoDisclaimer,
|
||||
fullName = model.params.config.cryptoCurrency.network.name,
|
||||
tokenName = model.getTokenName(),
|
||||
),
|
||||
)
|
||||
TokenReceiveRoutes.Warning -> TokenReceiveWarningComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = TokenReceiveWarningComponent.TokenReceiveWarningParams(
|
||||
iconState = model.state.value.iconState,
|
||||
callback = model,
|
||||
onDismiss = ::dismiss,
|
||||
network = model.params.config.cryptoCurrency.network,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitialConfig(shouldShowWarning: Boolean): TokenReceiveRoutes {
|
||||
return if (shouldShowWarning) {
|
||||
TokenReceiveRoutes.Warning
|
||||
} else {
|
||||
TokenReceiveRoutes.ReceiveAssets
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TokenReceiveComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: TokenReceiveComponent.Params,
|
||||
): DefaultTokenReceiveComponent
|
||||
}
|
||||
}
|
||||
|
||||
internal interface TokenReceiveModelCallback :
|
||||
TokenReceiveAssetsComponent.TokenReceiveAssetsModelCallback,
|
||||
TokenReceiveQrCodeComponent.TokenReceiveQrCodeModelCallback,
|
||||
TokenReceiveWarningComponent.TokenReceiveWarningModelCallback
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.tokenreceive.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
|
||||
import com.tangem.features.tokenreceive.ui.TokenReceiveAssetsContent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
internal class TokenReceiveAssetsComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: TokenReceiveAssetsParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TokenReceiveAssetsModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
TokenReceiveAssetsContent(assetsUM = state)
|
||||
}
|
||||
|
||||
internal interface TokenReceiveAssetsModelCallback {
|
||||
fun onQrCodeClick(id: Int)
|
||||
fun onCopyClick(id: Int)
|
||||
}
|
||||
|
||||
data class TokenReceiveAssetsParams(
|
||||
val notificationConfigs: ImmutableList<NotificationUM>,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val callback: TokenReceiveAssetsModelCallback,
|
||||
val onDismiss: () -> Unit,
|
||||
val showMemoDisclaimer: Boolean,
|
||||
val fullName: String,
|
||||
val tokenName: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.features.tokenreceive.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel
|
||||
import com.tangem.features.tokenreceive.ui.TokenReceiveQrCodeContent
|
||||
|
||||
internal class TokenReceiveQrCodeComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: TokenReceiveQrCodeParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TokenReceiveQrCodeModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
TokenReceiveQrCodeContent(qrCodeUM = state)
|
||||
}
|
||||
|
||||
internal interface TokenReceiveQrCodeModelCallback {
|
||||
fun onCopyClick(id: Int)
|
||||
fun onShareClick(address: String)
|
||||
}
|
||||
|
||||
data class TokenReceiveQrCodeParams(
|
||||
val id: Int,
|
||||
val network: String,
|
||||
val address: ReceiveAddress,
|
||||
val callback: TokenReceiveQrCodeModelCallback,
|
||||
val onDismiss: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.features.tokenreceive.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel
|
||||
import com.tangem.features.tokenreceive.ui.TokenReceiveWarningContent
|
||||
|
||||
internal class TokenReceiveWarningComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: TokenReceiveWarningParams,
|
||||
) : AppComponentContext by appComponentContext, ComposableContentComponent {
|
||||
|
||||
private val model: TokenReceiveWarningModel = getOrCreateModel(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
TokenReceiveWarningContent(warningUM = state)
|
||||
}
|
||||
|
||||
internal interface TokenReceiveWarningModelCallback {
|
||||
fun onWarningAcknowledged()
|
||||
}
|
||||
|
||||
data class TokenReceiveWarningParams(
|
||||
val iconState: CurrencyIconState,
|
||||
val callback: TokenReceiveWarningModelCallback,
|
||||
val onDismiss: () -> Unit,
|
||||
val network: Network,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.tokenreceive.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.tokenreceive.DefaultTokenReceiveFeatureToggle
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.features.tokenreceive.component.DefaultTokenReceiveComponent
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveAssetsModel
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveModel
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveQrCodeModel
|
||||
import com.tangem.features.tokenreceive.model.TokenReceiveWarningModel
|
||||
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
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object FeatureToggleModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenReceiveFeatureToggle(featureTogglesManager: FeatureTogglesManager): TokenReceiveFeatureToggle {
|
||||
return DefaultTokenReceiveFeatureToggle(
|
||||
featureTogglesManager = featureTogglesManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponent(factory: DefaultTokenReceiveComponent.Factory): TokenReceiveComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenReceiveModel::class)
|
||||
fun bindsTokenReceiveModel(model: TokenReceiveModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenReceiveAssetsModel::class)
|
||||
fun bindsTokenReceiveAssetsModel(model: TokenReceiveAssetsModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenReceiveQrCodeModel::class)
|
||||
fun bindsTokenReceiveQrCodeModel(model: TokenReceiveQrCodeModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TokenReceiveWarningModel::class)
|
||||
fun bindsTokenReceiveWarningModel(model: TokenReceiveWarningModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.tokenreceive.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class ReceiveAddress(
|
||||
val value: String,
|
||||
val type: Type,
|
||||
) {
|
||||
sealed interface Type {
|
||||
data object Ens : Type
|
||||
|
||||
data class Default(
|
||||
val displayName: TextReference,
|
||||
) : Type
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.features.tokenreceive.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
|
||||
import com.tangem.features.tokenreceive.component.TokenReceiveAssetsComponent
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TokenReceiveAssetsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenReceiveAssetsComponent.TokenReceiveAssetsParams>()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(
|
||||
TokenReceiveNewAnalyticsEvent.ReceiveScreenOpened(
|
||||
token = params.tokenName,
|
||||
blockchainName = params.fullName,
|
||||
ensStatus = configureEnsStatus(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal val state: StateFlow<ReceiveAssetsUM>
|
||||
field = MutableStateFlow<ReceiveAssetsUM>(
|
||||
ReceiveAssetsUM(
|
||||
onCopyClick = params.callback::onCopyClick,
|
||||
onOpenQrCodeClick = params.callback::onQrCodeClick,
|
||||
addresses = params.addresses,
|
||||
showMemoDisclaimer = params.showMemoDisclaimer,
|
||||
isEnsResultLoading = false,
|
||||
notificationConfigs = params.notificationConfigs,
|
||||
fullName = params.fullName,
|
||||
),
|
||||
)
|
||||
|
||||
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
|
||||
val hasEnsAddress = params.addresses.values.any { it.type == ReceiveAddress.Type.Ens }
|
||||
return if (hasEnsAddress) {
|
||||
AnalyticsParam.EnsStatus.FULL
|
||||
} else {
|
||||
AnalyticsParam.EnsStatus.EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
package com.tangem.features.tokenreceive.model
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
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.core.navigation.share.ShareManager
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.models.Asset
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.ens.EnsAddress
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.tokens.SaveViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent
|
||||
import com.tangem.domain.transaction.usecase.GetReverseResolvedEnsAddressUseCase
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.tokenreceive.component.TokenReceiveModelCallback
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
|
||||
import com.tangem.features.tokenreceive.ui.state.TokenReceiveUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class TokenReceiveModel @Inject constructor(
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val getReverseResolvedEnsAddressUseCase: GetReverseResolvedEnsAddressUseCase,
|
||||
private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), TokenReceiveModelCallback {
|
||||
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
val params = paramsContainer.require<TokenReceiveComponent.Params>()
|
||||
|
||||
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
|
||||
|
||||
internal val state: StateFlow<TokenReceiveUM>
|
||||
field = MutableStateFlow<TokenReceiveUM>(getInitState())
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
load()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQrCodeClick(id: Int) {
|
||||
analyticsEventHandler.send(
|
||||
TokenReceiveNewAnalyticsEvent.QrScreenOpened(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
stackNavigation.push(configuration = TokenReceiveRoutes.QrCode(addressId = id))
|
||||
}
|
||||
|
||||
override fun onCopyClick(id: Int) {
|
||||
val addressToCopy = state.value.addresses[id] ?: return
|
||||
sendCopyActionAnalytic(addressToCopy)
|
||||
clipboardManager.setText(text = addressToCopy.value, isSensitive = true)
|
||||
}
|
||||
|
||||
override fun onShareClick(address: String) {
|
||||
shareManager.shareText(text = address)
|
||||
}
|
||||
|
||||
override fun onWarningAcknowledged() {
|
||||
modelScope.launch {
|
||||
saveViewedTokenReceiveWarningUseCase.invoke(
|
||||
when (val asset = params.config.asset) {
|
||||
Asset.Currency -> params.config.cryptoCurrency.name
|
||||
Asset.NFT -> asset.name
|
||||
},
|
||||
)
|
||||
stackNavigation.push(configuration = TokenReceiveRoutes.ReceiveAssets)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun getTokenName(): String {
|
||||
return when (val asset = params.config.asset) {
|
||||
Asset.Currency -> params.config.cryptoCurrency.symbol
|
||||
Asset.NFT -> asset.name
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapAddresses(addresses: List<ReceiveAddressModel>): ImmutableMap<Int, ReceiveAddress> {
|
||||
return buildMap {
|
||||
addresses.mapIndexed { index, model ->
|
||||
val type = when (model.nameService) {
|
||||
ReceiveAddressModel.NameService.Default -> {
|
||||
ReceiveAddress.Type.Default(
|
||||
displayName = stringReference(model.displayName),
|
||||
)
|
||||
}
|
||||
ReceiveAddressModel.NameService.Ens -> ReceiveAddress.Type.Ens
|
||||
}
|
||||
put(
|
||||
key = index,
|
||||
value = ReceiveAddress(
|
||||
value = model.value,
|
||||
type = type,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentMap()
|
||||
}
|
||||
|
||||
private suspend fun load() = withContext(dispatchers.default) {
|
||||
state.value = state.value.copy(
|
||||
isEnsResultLoading = true,
|
||||
)
|
||||
|
||||
val reverseResolveResult =
|
||||
if (params.config.cryptoCurrency.network.nameResolvingType == Network.NameResolvingType.ENS) {
|
||||
getReverseResolvedEnsAddressUseCase(
|
||||
userWalletId = params.config.userWalletId,
|
||||
network = params.config.cryptoCurrency.network,
|
||||
addresses = params.config.receiveAddress.map { it.value },
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val currentAddressValues = state.value.addresses.values.map { it.value }.toSet()
|
||||
|
||||
val newEnsAddresses = reverseResolveResult
|
||||
.filterIsInstance<EnsAddress.Address>()
|
||||
.filterNot { it.name in currentAddressValues }
|
||||
.map { ensAddress -> ReceiveAddress(value = ensAddress.name, type = ReceiveAddress.Type.Ens) }
|
||||
|
||||
val combinedAddresses = (state.value.addresses.values + newEnsAddresses)
|
||||
.sortedWith(compareByDescending { it.type is ReceiveAddress.Type.Ens })
|
||||
|
||||
val updatedAddresses = combinedAddresses
|
||||
.mapIndexed { index, address -> index to address }
|
||||
.toMap()
|
||||
.toPersistentMap()
|
||||
|
||||
state.value = state.value.copy(
|
||||
isEnsResultLoading = false,
|
||||
addresses = updatedAddresses,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getNotifications(): List<NotificationUM> {
|
||||
return buildList {
|
||||
add(
|
||||
NotificationUM.Info(
|
||||
title = resourceReference(
|
||||
R.string.receive_bottom_sheet_warning_title,
|
||||
wrappedList(
|
||||
getTokenName(),
|
||||
params.config.cryptoCurrency.network.name,
|
||||
),
|
||||
),
|
||||
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
|
||||
iconTint = NotificationConfig.IconTint.Accent,
|
||||
),
|
||||
)
|
||||
|
||||
params.config.tokenReceiveNotification.map { notification ->
|
||||
add(
|
||||
NotificationUM.Warning(
|
||||
title = resourceReference(notification.title),
|
||||
subtitle = resourceReference(notification.subtitle),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInitState(): TokenReceiveUM {
|
||||
return TokenReceiveUM(
|
||||
addresses = mapAddresses(params.config.receiveAddress),
|
||||
iconState = iconStateConverter.convert(params.config.cryptoCurrency),
|
||||
network = params.config.cryptoCurrency.network.name,
|
||||
isEnsResultLoading = false,
|
||||
notificationConfigs = getNotifications().toImmutableList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendCopyActionAnalytic(receiveAddress: ReceiveAddress) {
|
||||
val event = when (receiveAddress.type) {
|
||||
is ReceiveAddress.Type.Default -> {
|
||||
TokenReceiveNewAnalyticsEvent.ButtonCopyAddress(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
)
|
||||
}
|
||||
ReceiveAddress.Type.Ens -> {
|
||||
TokenReceiveNewAnalyticsEvent.ButtonCopyEns(
|
||||
token = getTokenName(),
|
||||
blockchainName = params.config.cryptoCurrency.network.name,
|
||||
)
|
||||
}
|
||||
}
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.tokenreceive.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.tokenreceive.component.TokenReceiveQrCodeComponent
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TokenReceiveQrCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams>()
|
||||
|
||||
internal val state: StateFlow<QrCodeUM>
|
||||
field = MutableStateFlow<QrCodeUM>(
|
||||
QrCodeUM(
|
||||
network = params.network,
|
||||
addressValue = params.address.value,
|
||||
addressName = (params.address.type as? ReceiveAddress.Type.Default)?.displayName ?: TextReference.EMPTY,
|
||||
onCopyClick = { params.callback.onCopyClick(params.id) },
|
||||
onShareClick = params.callback::onShareClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.tokenreceive.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.extensions.iconResId
|
||||
import com.tangem.features.tokenreceive.component.TokenReceiveWarningComponent
|
||||
import com.tangem.features.tokenreceive.ui.state.WarningUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TokenReceiveWarningModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TokenReceiveWarningComponent.TokenReceiveWarningParams>()
|
||||
|
||||
internal val state: StateFlow<WarningUM>
|
||||
field = MutableStateFlow<WarningUM>(
|
||||
WarningUM(
|
||||
iconState = params.iconState,
|
||||
onWarningAcknowledged = params.callback::onWarningAcknowledged,
|
||||
network = params.network.name,
|
||||
networkIcon = params.network.iconResId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.tokenreceive.route
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Immutable
|
||||
@Serializable
|
||||
internal sealed interface TokenReceiveRoutes : Route {
|
||||
|
||||
@Serializable
|
||||
data object Warning : TokenReceiveRoutes
|
||||
|
||||
@Serializable
|
||||
data object ReceiveAssets : TokenReceiveRoutes
|
||||
|
||||
@Serializable
|
||||
data class QrCode(val addressId: Int) : TokenReceiveRoutes
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
|
||||
|
||||
@Composable
|
||||
internal fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
|
||||
Box {
|
||||
content()
|
||||
CopiedTextSnackbarHost(
|
||||
hostState = snackbarHostState,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 80.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.SpacerW8
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.buttons.small.TangemIconButton
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import com.tangem.features.tokenreceive.ui.state.ReceiveAssetsUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveAssetsContent(assetsUM: ReceiveAssetsUM) {
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
|
||||
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
),
|
||||
) {
|
||||
SpacerH8()
|
||||
|
||||
Info(
|
||||
showMemoDisclaimer = assetsUM.showMemoDisclaimer,
|
||||
notificationConfigs = assetsUM.notificationConfigs,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
AddressBlock(
|
||||
onCopyClick = assetsUM.onCopyClick,
|
||||
onOpenQrCodeClick = assetsUM.onOpenQrCodeClick,
|
||||
addresses = assetsUM.addresses,
|
||||
snackbarHostState = snackbarHostState,
|
||||
fullName = assetsUM.fullName,
|
||||
)
|
||||
|
||||
if (assetsUM.isEnsResultLoading) {
|
||||
LoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Info(
|
||||
notificationConfigs: ImmutableList<NotificationUM>,
|
||||
showMemoDisclaimer: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
|
||||
) {
|
||||
if (showMemoDisclaimer) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 18.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
notificationConfigs.fastForEach {
|
||||
key(it.hashCode()) {
|
||||
Notification(config = it.config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBlock(
|
||||
onOpenQrCodeClick: (id: Int) -> Unit,
|
||||
onCopyClick: (id: Int) -> Unit,
|
||||
addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fullName: String,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
addresses.entries.toList().fastForEach { entry ->
|
||||
when (entry.value.type) {
|
||||
is ReceiveAddress.Type.Default -> {
|
||||
key(entry.key) {
|
||||
AddressItem(
|
||||
onCopyClick = {
|
||||
onCopyClick(entry.key)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(
|
||||
R.string.wallet_notification_address_copied,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onOpenQrCodeClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onOpenQrCodeClick(entry.key)
|
||||
},
|
||||
fullName = fullName,
|
||||
address = entry.value.value,
|
||||
)
|
||||
SpacerH8()
|
||||
}
|
||||
}
|
||||
ReceiveAddress.Type.Ens -> {
|
||||
key(entry.key) {
|
||||
EnsItem(
|
||||
onCopyClick = {
|
||||
onCopyClick(entry.key)
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(
|
||||
R.string.wallet_notification_address_copied,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
address = entry.value.value,
|
||||
)
|
||||
SpacerH8()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressItem(
|
||||
onOpenQrCodeClick: () -> Unit,
|
||||
fullName: String,
|
||||
onCopyClick: () -> Unit,
|
||||
address: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
onClick = onOpenQrCodeClick,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 14.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IdentIcon(
|
||||
address = address,
|
||||
modifier = Modifier
|
||||
.size(size = 36.dp)
|
||||
.clip(shape = RoundedCornerShape(18.dp)),
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
EllipsisText(
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, fullName),
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
|
||||
EllipsisText(
|
||||
text = address,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerW12()
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size28),
|
||||
iconRes = R.drawable.ic_qrcode_new_24,
|
||||
onClick = onOpenQrCodeClick,
|
||||
)
|
||||
|
||||
SpacerW8()
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size28),
|
||||
iconRes = R.drawable.ic_copy_new_24,
|
||||
onClick = onCopyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnsItem(onCopyClick: () -> Unit, address: String, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 14.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.size(36.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_ens_36),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
EllipsisText(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = address,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
|
||||
TangemIconButton(
|
||||
modifier = Modifier.size(28.dp),
|
||||
iconRes = R.drawable.ic_copy_new_24,
|
||||
onClick = onCopyClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingBlock(modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 64.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveAssetsContent(
|
||||
@PreviewParameter(TokenReceiveAssetsContentProvider::class) params: ReceiveAssetsUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveAssetsContent(assetsUM = params)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveAssetsContentProvider : PreviewParameterProvider<ReceiveAssetsUM> {
|
||||
val address = ReceiveAddress(
|
||||
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
|
||||
type = ReceiveAddress.Type.Default(
|
||||
displayName = stringReference("Etherium address"),
|
||||
),
|
||||
)
|
||||
private val config = ReceiveAssetsUM(
|
||||
notificationConfigs =
|
||||
persistentListOf(
|
||||
NotificationUM.Warning(
|
||||
title = stringReference("Send only XLM on the Ethereum network"),
|
||||
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
|
||||
),
|
||||
),
|
||||
addresses = persistentMapOf(
|
||||
0 to address,
|
||||
1 to address.copy(type = ReceiveAddress.Type.Ens, value = "papasha.eth"),
|
||||
),
|
||||
showMemoDisclaimer = false,
|
||||
onCopyClick = {},
|
||||
onOpenQrCodeClick = {},
|
||||
isEnsResultLoading = true,
|
||||
fullName = "Etherium",
|
||||
)
|
||||
|
||||
override val values: Sequence<ReceiveAssetsUM>
|
||||
get() = sequenceOf(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.tokenreceive.route.TokenReceiveRoutes
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveContentSheet(
|
||||
route: TokenReceiveRoutes,
|
||||
onCloseClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
contentStack: ChildStack<TokenReceiveRoutes, ComposableContentComponent>,
|
||||
) {
|
||||
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = onCloseClick,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
),
|
||||
onBack = onBackClick,
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
title = {
|
||||
Title(
|
||||
route = route,
|
||||
onBackClick = onBackClick,
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
TokenReceiveContent(
|
||||
stackState = contentStack,
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(route: TokenReceiveRoutes, onBackClick: () -> Unit, onCloseClick: () -> Unit) {
|
||||
when (route) {
|
||||
is TokenReceiveRoutes.QrCode -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
startIconRes = R.drawable.ic_back_24,
|
||||
onStartClick = onBackClick,
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
TokenReceiveRoutes.ReceiveAssets -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
title = resourceReference(R.string.domain_receive_assets_navigation_title),
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
TokenReceiveRoutes.Warning -> {
|
||||
TangemModalBottomSheetTitle(
|
||||
endIconRes = R.drawable.ic_close_24,
|
||||
onEndClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveContent(
|
||||
stackState: ChildStack<TokenReceiveRoutes, ComposableContentComponent>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(fade()),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.animateContentSize(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.impl.R
|
||||
import com.tangem.features.tokenreceive.ui.state.QrCodeUM
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveQrCodeContent(qrCodeUM: QrCodeUM) {
|
||||
val snackbarHostState = remember(::SnackbarHostState)
|
||||
val qrCodePainter = rememberQrPainter(content = qrCodeUM.addressValue)
|
||||
|
||||
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(bottom = 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerH8()
|
||||
|
||||
QrCodePage(
|
||||
addressFullName = qrCodeUM.addressName,
|
||||
addressValue = qrCodeUM.addressValue,
|
||||
network = qrCodeUM.network,
|
||||
qrCodePainter = qrCodePainter,
|
||||
)
|
||||
SpacerH24()
|
||||
|
||||
Buttons(
|
||||
onShareClick = { qrCodeUM.onShareClick(qrCodeUM.addressValue) },
|
||||
onCopyClick = qrCodeUM.onCopyClick,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QrCodePage(addressFullName: TextReference, addressValue: String, network: String, qrCodePainter: Painter) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.size36)) {
|
||||
Text(
|
||||
text = stringResourceSafe(
|
||||
R.string.receive_bottom_sheet_warning_message_compact,
|
||||
addressFullName.resolveReference(),
|
||||
network,
|
||||
),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
Image(
|
||||
painter = qrCodePainter,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(248.dp),
|
||||
)
|
||||
|
||||
SpacerH24()
|
||||
}
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.wc_common_address),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
SpacerH2()
|
||||
|
||||
Text(
|
||||
text = addressValue,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Buttons(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
onShareClick: () -> Unit,
|
||||
onCopyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(id = R.string.common_copy),
|
||||
iconResId = R.drawable.ic_copy_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onCopyClick()
|
||||
coroutineScope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResourceSafe(id = R.string.common_share),
|
||||
iconResId = R.drawable.ic_share_24,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onShareClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberQrPainter(content: String, size: Dp = 248.dp, padding: Dp = 0.dp): BitmapPainter {
|
||||
val density = LocalDensity.current
|
||||
return remember(content) {
|
||||
BitmapPainter(
|
||||
content.toQrCode(
|
||||
sizePx = with(density) { size.roundToPx() },
|
||||
paddingPx = with(density) { padding.roundToPx() },
|
||||
).asImageBitmap(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveQrCodeContent(
|
||||
@PreviewParameter(TokenReceiveQrCodeContentPreviewProvider::class) qrCodeUM: QrCodeUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveQrCodeContent(qrCodeUM = qrCodeUM)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveQrCodeContentPreviewProvider : PreviewParameterProvider<QrCodeUM> {
|
||||
private val config = QrCodeUM(
|
||||
network = "Ethereum",
|
||||
addressName = stringReference("Etherium"),
|
||||
addressValue = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
|
||||
onCopyClick = {},
|
||||
onShareClick = {},
|
||||
)
|
||||
|
||||
override val values: Sequence<QrCodeUM>
|
||||
get() = sequenceOf(config)
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package com.tangem.features.tokenreceive.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.tokenreceive.impl.R
|
||||
import com.tangem.features.tokenreceive.ui.state.WarningUM
|
||||
|
||||
@Composable
|
||||
internal fun TokenReceiveWarningContent(warningUM: WarningUM) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
CurrencyIcon(
|
||||
modifier = Modifier.size(size = 56.dp),
|
||||
state = warningUM.iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
iconSize = 56.dp,
|
||||
)
|
||||
|
||||
SpacerH24()
|
||||
|
||||
WarningBlock(networkIcon = warningUM.networkIcon, networkName = warningUM.network)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_description),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
SpacerH(48.dp)
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.common_got_it),
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
warningUM.onWarningAcknowledged()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WarningBlock(networkName: String, networkIcon: Int, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
painter = painterResource(id = networkIcon),
|
||||
tint = Color.Unspecified,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResourceSafe(R.string.domain_receive_assets_onboarding_network_name, networkName),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_TokenReceiveWarningContent(
|
||||
@PreviewParameter(TokenReceiveWarningContentProvider::class) warningUM: WarningUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenReceiveWarningContent(warningUM = warningUM)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokenReceiveWarningContentProvider : PreviewParameterProvider<WarningUM> {
|
||||
val iconState = CurrencyIconState.TokenIcon(
|
||||
url = null,
|
||||
topBadgeIconResId = null,
|
||||
fallbackTint = TangemColorPalette.Black,
|
||||
fallbackBackground = TangemColorPalette.Meadow,
|
||||
isGrayscale = false,
|
||||
showCustomBadge = false,
|
||||
)
|
||||
|
||||
override val values: Sequence<WarningUM>
|
||||
get() = sequenceOf(
|
||||
WarningUM(
|
||||
iconState = iconState,
|
||||
onWarningAcknowledged = {},
|
||||
network = "Etherium",
|
||||
networkIcon = R.drawable.ic_eth_16,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class QrCodeUM(
|
||||
val onCopyClick: () -> Unit,
|
||||
val onShareClick: (String) -> Unit,
|
||||
val addressName: TextReference,
|
||||
val addressValue: String,
|
||||
val network: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
internal data class ReceiveAssetsUM(
|
||||
val showMemoDisclaimer: Boolean,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val onOpenQrCodeClick: (id: Int) -> Unit,
|
||||
val onCopyClick: (id: Int) -> Unit,
|
||||
val isEnsResultLoading: Boolean,
|
||||
val notificationConfigs: ImmutableList<NotificationUM>,
|
||||
val fullName: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.features.tokenreceive.entity.ReceiveAddress
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
||||
internal data class TokenReceiveUM(
|
||||
val network: String,
|
||||
val iconState: CurrencyIconState,
|
||||
val addresses: ImmutableMap<Int, ReceiveAddress>,
|
||||
val isEnsResultLoading: Boolean,
|
||||
val notificationConfigs: ImmutableList<NotificationUM>,
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.tokenreceive.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
|
||||
internal data class WarningUM(
|
||||
val network: String,
|
||||
val networkIcon: Int,
|
||||
val iconState: CurrencyIconState,
|
||||
val onWarningAcknowledged: () -> Unit,
|
||||
)
|
||||
|
|
@ -101,5 +101,8 @@ dependencies {
|
|||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.txhistory.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
}
|
||||
|
|
@ -4,16 +4,24 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
|
||||
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -25,6 +33,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
@Assisted params: TokenDetailsComponent.Params,
|
||||
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
|
||||
txHistoryComponentFactory: TxHistoryComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TokenDetailsModel = getOrCreateModel(params)
|
||||
|
|
@ -37,6 +46,13 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onPause = model::onPause,
|
||||
|
|
@ -54,12 +70,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
NavigationBar3ButtonsScrim()
|
||||
TokenDetailsScreen(
|
||||
state = state,
|
||||
tokenMarketBlockComponent = tokenMarketBlockComponent,
|
||||
txHistoryComponent = txHistoryComponent,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toTokenMarketParam(): TokenMarketBlockComponent.Params? {
|
||||
|
|
@ -68,6 +86,17 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
return TokenMarketBlockComponent.Params(cryptoCurrency = this)
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TokenDetailsComponent.Factory {
|
||||
override fun create(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.paging.cachedIn
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.merge
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.blockchain.common.address.AddressType
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
|
|
@ -35,8 +37,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
|||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -60,10 +65,7 @@ import com.tangem.domain.transaction.error.AssociateAssetError
|
|||
import com.tangem.domain.transaction.error.IncompleteTransactionError
|
||||
import com.tangem.domain.transaction.error.OpenTrustlineError
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.AssociateAssetUseCase
|
||||
import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.OpenTrustlineUseCase
|
||||
import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
|
|
@ -82,6 +84,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.e
|
|||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -137,6 +140,9 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val sendFeatureToggles: SendFeatureToggles,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : Model(), TokenDetailsClickIntents {
|
||||
|
||||
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
|
||||
|
|
@ -159,6 +165,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
/** Transaction id to check for status */
|
||||
private val waitForFirstExpressStatusEmmit = MutableStateFlow(false)
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val stateFactory = TokenDetailsStateFactory(
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
|
|
@ -594,20 +602,27 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
modelScope.launch {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
|
||||
|
||||
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
|
||||
currency = cryptoCurrency,
|
||||
networkAddress = networkAddress,
|
||||
onCopyClick = {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
|
||||
clipboardManager.setText(text = it, isSensitive = true)
|
||||
},
|
||||
onShareClick = {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol))
|
||||
shareManager.shareText(text = it)
|
||||
},
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
bottomSheetNavigation.activate(
|
||||
configuration = configureReceiveAddresses(addresses = networkAddress),
|
||||
)
|
||||
} else {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol))
|
||||
internalUiState.value = stateFactory.getStateWithReceiveBottomSheet(
|
||||
currency = cryptoCurrency,
|
||||
networkAddress = networkAddress,
|
||||
onCopyClick = {
|
||||
analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol))
|
||||
clipboardManager.setText(text = it, isSensitive = true)
|
||||
},
|
||||
onShareClick = {
|
||||
analyticsEventsHandler.send(
|
||||
TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol),
|
||||
)
|
||||
shareManager.shareText(text = it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1061,6 +1076,43 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(addresses: NetworkAddress): TokenReceiveConfig {
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrency.name} (${cryptoCurrency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = userWalletId,
|
||||
showMemoDisclaimer = cryptoCurrency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.notifications)
|
||||
implementation(projects.domain.notifications.toggles)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.details.api)
|
||||
|
|
@ -114,6 +115,7 @@ dependencies {
|
|||
implementation(projects.features.nft.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
implementation(projects.features.kyc.api)
|
||||
implementation(projects.features.tokenRecieve.api)
|
||||
|
||||
/** Common modules */
|
||||
implementation(projects.common)
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ import com.tangem.features.biometry.AskBiometryComponent
|
|||
import com.tangem.features.markets.entry.MarketsEntryComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted navigate: (WalletRoute) -> Unit,
|
||||
|
|
@ -36,6 +38,7 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
private val marketsEntryComponentFactory: MarketsEntryComponent.Factory,
|
||||
private val askBiometryComponentFactory: AskBiometryComponent.Factory,
|
||||
private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: WalletModel = getOrCreateModel()
|
||||
|
|
@ -78,6 +81,15 @@ internal class WalletComponent @AssistedInject constructor(
|
|||
source = AppRoute.PushNotification.Source.Main,
|
||||
),
|
||||
)
|
||||
is WalletDialogConfig.TokenReceive -> {
|
||||
tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = dialogConfig.tokenReceiveConfig,
|
||||
onDismiss = model.innerWalletRouter.dialogNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import com.tangem.domain.core.utils.lceError
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -41,6 +43,7 @@ import com.tangem.domain.promo.models.StoryContentIds
|
|||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
|
||||
import com.tangem.domain.tokens.RemoveCurrencyUseCase
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
|
|
@ -49,6 +52,7 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
|||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.AVAILABLE
|
||||
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Companion.toReasonAnalyticsText
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -61,6 +65,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -128,6 +133,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
private val shareManager: ShareManager,
|
||||
private val appRouter: AppRouter,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents {
|
||||
|
||||
override fun onSendClick(
|
||||
|
|
@ -172,13 +180,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
event?.let { analyticsEventHandler.send(it) }
|
||||
|
||||
stateHolder.showBottomSheet(
|
||||
createReceiveBottomSheetContent(
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
|
||||
),
|
||||
userWalletId,
|
||||
)
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
stateHolder.hideBottomSheet()
|
||||
modelScope.launch {
|
||||
configureReceiveAddresses(cryptoCurrencyStatus = cryptoCurrencyStatus)?.let {
|
||||
router.openTokenReceiveBottomSheet(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stateHolder.showBottomSheet(
|
||||
createReceiveBottomSheetContent(
|
||||
currency = cryptoCurrencyStatus.currency,
|
||||
addresses = cryptoCurrencyStatus.value.networkAddress ?: return,
|
||||
),
|
||||
userWalletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? {
|
||||
|
|
@ -639,4 +656,45 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
|
|||
targetRoute
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? {
|
||||
val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return null
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
address = networkAddress.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
networkAddress.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
userWalletId = userWalletId,
|
||||
showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.router
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -98,4 +100,10 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig) {
|
||||
dialogNavigation.activate(
|
||||
configuration = WalletDialogConfig.TokenReceive(tokenReceiveConfig),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.router
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -54,4 +55,6 @@ internal interface InnerWalletRouter {
|
|||
|
||||
/** Open NFT collections screen */
|
||||
fun openNFT(userWallet: UserWallet)
|
||||
|
||||
fun openTokenReceiveBottomSheet(tokenReceiveConfig: TokenReceiveConfig)
|
||||
}
|
||||
|
|
@ -90,6 +90,16 @@ internal class WalletStateController @Inject constructor() {
|
|||
)
|
||||
}
|
||||
|
||||
fun hideBottomSheet() {
|
||||
update(
|
||||
transformer = OpenBottomSheetTransformer(
|
||||
userWalletId = getSelectedWalletId(),
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
onDismissBottomSheet = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getInitialState(): WalletScreenState {
|
||||
return WalletScreenState(
|
||||
topBarConfig = WalletTopBarConfig(onDetailsClick = {}),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
|
@ -19,4 +20,7 @@ internal sealed interface WalletDialogConfig {
|
|||
|
||||
@Serializable
|
||||
data object AskForPushNotifications : WalletDialogConfig
|
||||
|
||||
@Serializable
|
||||
data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue