Updated on 2026-08-14
This commit is contained in:
parent
d7c4218f92
commit
5efb1956c7
30 changed files with 1768 additions and 5 deletions
|
|
@ -12,8 +12,4 @@ interface Transformer<S> {
|
|||
|
||||
fun <T> MutableStateFlow<T>.update(transformer: Transformer<T>) {
|
||||
update { transformer.transform(this.value) }
|
||||
}
|
||||
|
||||
fun <T> MutableStateFlow<T>.updateAll(vararg transformers: Transformer<T>) {
|
||||
transformers.forEach { transformer -> update { transformer.transform(this.value) } }
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface SendComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val transactionId: String? = null,
|
||||
val amount: String? = null,
|
||||
val tag: String? = null,
|
||||
val destinationAddress: String? = null,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendComponent>
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.features.send.v2
|
|||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.send.v2.api.SendFeatureToggles
|
||||
|
||||
class DefaultSendFeatureToggles(
|
||||
internal class DefaultSendFeatureToggles(
|
||||
private val featureToggles: FeatureTogglesManager,
|
||||
) : SendFeatureToggles {
|
||||
override val isSendV2Enabled: Boolean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface SendModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendDestinationModel::class)
|
||||
fun provideSendDestinationModel(model: SendDestinationModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.send.v2.send
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
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.send.v2.api.SendComponent
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
|
||||
internal class DefaultSendComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: SendComponent.Params,
|
||||
) : SendComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val stackNavigation = StackNavigation<SendRoute>()
|
||||
|
||||
private val innerRouter = InnerRouter<SendRoute>(
|
||||
stackNavigation = stackNavigation,
|
||||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val initialRoute = SendRoute.Empty
|
||||
private val currentRoute = MutableStateFlow<SendRoute>(initialRoute)
|
||||
|
||||
private val model: SendModel = getOrCreateModel(params = params, router = innerRouter)
|
||||
|
||||
private val childStack = childStack(
|
||||
key = "sendInnerStack",
|
||||
source = stackNavigation,
|
||||
serializer = null,
|
||||
initialConfiguration = initialRoute,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
createChild(
|
||||
configuration,
|
||||
childByContext(
|
||||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
}
|
||||
|
||||
private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) {
|
||||
SendRoute.Empty -> getStubComponent()
|
||||
is SendRoute.Destination -> getDestinationComponent(factoryContext, route)
|
||||
is SendRoute.Amount -> getAmountComponent()
|
||||
is SendRoute.Fee -> getFeeComponent()
|
||||
SendRoute.Confirm -> getConfirmComponent()
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) =
|
||||
SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponent.Params(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRoute.filterIsInstance<SendRoute.Destination>(),
|
||||
analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY,
|
||||
userWallet = model.userWallet,
|
||||
cryptoCurrencyStatus = model.cryptoCurrencyStatus,
|
||||
callback = model,
|
||||
isEditMode = route.isEditMode,
|
||||
),
|
||||
)
|
||||
|
||||
private fun getAmountComponent() = getStubComponent() // todo
|
||||
|
||||
private fun getFeeComponent() = getStubComponent() // todo
|
||||
|
||||
private fun getConfirmComponent() = getStubComponent() // todo
|
||||
|
||||
private fun getStubComponent() = ComposableContentComponent { }
|
||||
|
||||
private fun onChildBack() {
|
||||
if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) {
|
||||
router.pop()
|
||||
} else {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : SendComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: SendComponent.Params): DefaultSendComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.v2.send
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class SendRoute : Route {
|
||||
|
||||
abstract val isEditMode: Boolean
|
||||
|
||||
@Serializable
|
||||
data object Empty : SendRoute() {
|
||||
override val isEditMode = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Confirm : SendRoute() {
|
||||
override val isEditMode: Boolean = false
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Destination(
|
||||
override val isEditMode: Boolean,
|
||||
) : SendRoute()
|
||||
|
||||
@Serializable
|
||||
data class Amount(
|
||||
override val isEditMode: Boolean,
|
||||
) : SendRoute()
|
||||
|
||||
@Serializable
|
||||
data class Fee(
|
||||
override val isEditMode: Boolean = true,
|
||||
) : SendRoute()
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.send.v2.send.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
/**
|
||||
* Send screen analytics
|
||||
*/
|
||||
internal sealed class SendAnalyticEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) {
|
||||
|
||||
/** Recipient address screen opened */
|
||||
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
|
||||
|
||||
/** Amount screen opened */
|
||||
data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
|
||||
|
||||
/** Fee screen opened */
|
||||
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
|
||||
|
||||
/** Confirmation screen opened */
|
||||
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
|
||||
|
||||
/** If transaction delays notification is present */
|
||||
data class NoticeTransactionDelays(
|
||||
val token: String,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Notice - Transaction Delays Are Possible",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
/** If error occurs during send transactions */
|
||||
data class TransactionError(val token: String) : SendAnalyticEvents(
|
||||
event = "Error - Transaction Rejected",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val SEND_CATEGORY = "Token / Send"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.send.v2.send.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
) : Model(), SendDestinationComponent.ModelCallback {
|
||||
|
||||
private val _uiState = MutableStateFlow(initialState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
var userWallet: UserWallet by Delegates.notNull()
|
||||
var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
|
||||
|
||||
private fun initialState(): SendUM = SendUM(
|
||||
amountState = AmountState.Empty(),
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
)
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.send.v2.send.ui.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
|
||||
internal data class SendUM(
|
||||
val amountState: AmountState,
|
||||
val destinationUM: DestinationUM,
|
||||
)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
internal class SendDestinationComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendDestinationModel = getOrCreateModel(params = params, router = router)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state = model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SendDestinationContent(state = state.value, model, isBalanceHidden = false)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: DestinationUM,
|
||||
val analyticsCategoryName: String,
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<SendRoute.Destination>,
|
||||
val isEditMode: Boolean,
|
||||
)
|
||||
|
||||
interface ModelCallback {
|
||||
fun onDestinationResult(destinationUM: DestinationUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.analytics
|
||||
|
||||
internal enum class EnterAddressSource {
|
||||
QRCode,
|
||||
PasteButton,
|
||||
RecentAddress,
|
||||
InputField,
|
||||
;
|
||||
|
||||
val isPasted: Boolean
|
||||
get() = this != InputField
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
|
||||
|
||||
internal sealed class SendDestinationAnalyticEvents(
|
||||
category: String,
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = category, event = event, params = params) {
|
||||
|
||||
abstract val categoryName: String
|
||||
|
||||
/** Address to send entered */
|
||||
data class AddressEntered(
|
||||
override val categoryName: String,
|
||||
val source: EnterAddressSource,
|
||||
val isValid: Boolean,
|
||||
) : SendDestinationAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Address Entered",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
VALIDATION to if (isValid) "Success" else "Fail",
|
||||
),
|
||||
)
|
||||
|
||||
/** Qr Code button clicked */
|
||||
data class QrCodeButtonClicked(
|
||||
override val categoryName: String,
|
||||
) :
|
||||
SendDestinationAnalyticEvents(
|
||||
category = categoryName,
|
||||
event = "Button - QR Code",
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
|
||||
internal interface SendDestinationClickIntents {
|
||||
|
||||
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false)
|
||||
|
||||
fun onQrCodeScanClick()
|
||||
}
|
||||
|
|
@ -0,0 +1,306 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
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.decompose.navigation.Router
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
|
||||
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.GetNetworkAddressesUseCase
|
||||
import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.*
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendDestinationModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val validateWalletAddressUseCase: ValidateWalletAddressUseCase,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
|
||||
private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase,
|
||||
private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase,
|
||||
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
|
||||
private val parseQrCodeUseCase: ParseQrCodeUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model(), SendDestinationClickIntents {
|
||||
private val params: SendDestinationComponent.Params = paramsContainer.require()
|
||||
|
||||
private val _uiState = MutableStateFlow(params.state)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val analyticsCategoryName = params.analyticsCategoryName
|
||||
private val userWallet = params.userWallet
|
||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
private val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
private var validationJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
configDestinationNavigation()
|
||||
initialState()
|
||||
getWalletsAndRecent()
|
||||
subscribeOnQRScannerResult()
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
if (uiState.value is DestinationUM.Empty) {
|
||||
_uiState.update(
|
||||
SendDestinationInitialStateTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onAddressChange = ::onRecipientAddressValueChange,
|
||||
onMemoChange = ::onRecipientMemoValueChange,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateState(state: DestinationUM) {
|
||||
if (state !is DestinationUM.Empty) {
|
||||
_uiState.value = state
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) {
|
||||
_uiState.update(
|
||||
SendDestinationAddressTransformer(
|
||||
address = value,
|
||||
isPasted = type.isPasted,
|
||||
),
|
||||
)
|
||||
val memo = (uiState.value as? DestinationUM.Content)?.memoTextField?.value
|
||||
validate(address = value, memo = memo, type)
|
||||
}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
||||
_uiState.update(
|
||||
SendDestinationMemoTransformer(
|
||||
memo = value,
|
||||
isPasted = isValuePasted,
|
||||
),
|
||||
)
|
||||
val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.value.orEmpty()
|
||||
validate(address = address, memo = value)
|
||||
}
|
||||
|
||||
override fun onQrCodeScanClick() {
|
||||
analyticsEventHandler.send(SendDestinationAnalyticEvents.QrCodeButtonClicked(analyticsCategoryName))
|
||||
router.push(
|
||||
AppRoute.QrScanning(
|
||||
source = AppRoute.QrScanning.Source.SEND,
|
||||
networkName = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun subscribeOnQRScannerResult() {
|
||||
listenToQrScanningUseCase(SourceType.SEND)
|
||||
.getOrElse { emptyFlow() }
|
||||
.onEach(::onQrCodeScanned)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onQrCodeScanned(address: String) {
|
||||
parseQrCodeUseCase(address, cryptoCurrency).fold(
|
||||
ifRight = { parsedCode ->
|
||||
onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode)
|
||||
parsedCode.memo?.let { onRecipientMemoValueChange(it) }
|
||||
},
|
||||
ifLeft = {
|
||||
onRecipientAddressValueChange(address, EnterAddressSource.QRCode)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getWalletsAndRecent() {
|
||||
combine(
|
||||
flow = getWalletsUseCase().conflate().map {
|
||||
it.toAvailableWallets()
|
||||
},
|
||||
flow2 = getFixedTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
pageSize = RECENT_TX_SIZE,
|
||||
).getOrElse { flowOf(emptyList()) }.conflate(),
|
||||
) { destinationWalletList, txHistoryList ->
|
||||
val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
_uiState.update(
|
||||
SendDestinationRecentListTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
isUtxoConsolidationAvailable = isUtxoConsolidationAvailable,
|
||||
destinationWalletList = destinationWalletList,
|
||||
txHistoryList,
|
||||
),
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun List<UserWallet>.toAvailableWallets(): List<DestinationWalletUM> {
|
||||
return coroutineScope {
|
||||
val cryptoCurrencyNetwork = cryptoCurrency.network
|
||||
|
||||
return@coroutineScope filterNot { it.isLocked }
|
||||
.map { wallet ->
|
||||
async {
|
||||
val addresses = if (!wallet.isMultiCurrency) {
|
||||
getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let {
|
||||
if (it.network.id == cryptoCurrencyNetwork.id) {
|
||||
getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrencyNetwork)
|
||||
}
|
||||
wallet to addresses
|
||||
}
|
||||
}.awaitAll()
|
||||
.asSequence()
|
||||
.mapNotNull { (wallet, addresses) ->
|
||||
addresses?.map { (cryptoCurrency, address) ->
|
||||
DestinationWalletUM(
|
||||
name = wallet.name,
|
||||
address = address,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
userWalletId = wallet.walletId,
|
||||
)
|
||||
}
|
||||
}.flatten()
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) {
|
||||
modelScope.launch {
|
||||
_uiState.update(SendDestinationValidationStartedTransformer)
|
||||
|
||||
val addressValidationResult = validateWalletAddressUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
address = address,
|
||||
currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses,
|
||||
)
|
||||
val memoValidationResult = validateWalletMemoUseCase(
|
||||
memo = memo.orEmpty(),
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
type?.let {
|
||||
analyticsEventHandler.send(
|
||||
SendDestinationAnalyticEvents.AddressEntered(
|
||||
categoryName = analyticsCategoryName,
|
||||
source = it,
|
||||
isValid = addressValidationResult.isRight(),
|
||||
),
|
||||
)
|
||||
}
|
||||
_uiState.update(SendDestinationValidationResultTransformer(addressValidationResult, memoValidationResult))
|
||||
autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight())
|
||||
}.saveIn(validationJobHolder)
|
||||
}
|
||||
|
||||
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) {
|
||||
val isRecent = type == EnterAddressSource.RecentAddress
|
||||
if (isRecent && isValidAddress && isValidMemo) onNextClick()
|
||||
}
|
||||
|
||||
private fun saveResult() {
|
||||
params.callback.onDestinationResult(uiState.value)
|
||||
}
|
||||
|
||||
private fun onNextClick() {
|
||||
saveResult()
|
||||
if (params.isEditMode) {
|
||||
router.pop()
|
||||
} else {
|
||||
router.push(SendRoute.Amount(isEditMode = false))
|
||||
}
|
||||
}
|
||||
|
||||
private fun configDestinationNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach {
|
||||
// todo
|
||||
// params.callback.onNavigationResult(
|
||||
// NavigationUM.Content(
|
||||
// title = resourceReference(R.string.send_recipient_label),
|
||||
// subtitle = null,
|
||||
// backIconRes = if (route.isEditMode) {
|
||||
// R.drawable.ic_back_24
|
||||
// } else {
|
||||
// R.drawable.ic_close_24
|
||||
// },
|
||||
// backIconClick = {
|
||||
// if (route.isEditMode) {
|
||||
// router.pop()
|
||||
// } else {
|
||||
// appRouter.pop()
|
||||
// }
|
||||
// },
|
||||
// additionalIconRes = R.drawable.ic_qrcode_scan_24,
|
||||
// additionalIconClick = {
|
||||
// router.push(
|
||||
// AppRoute.QrScanning(
|
||||
// source = AppRoute.QrScanning.Source.SEND,
|
||||
// networkName = cryptoCurrency.network.name,
|
||||
// ),
|
||||
// )
|
||||
// },
|
||||
// primaryButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = if (route.isEditMode) {
|
||||
// resourceReference(R.string.common_continue)
|
||||
// } else {
|
||||
// resourceReference(R.string.common_next)
|
||||
// },
|
||||
// isEnabled = state.isPrimaryButtonEnabled,
|
||||
// onClick = ::onNextClick,
|
||||
// ),
|
||||
// prevButton = null,
|
||||
// secondaryPairButtonsUM = null,
|
||||
// ),
|
||||
// )
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val RECENT_TX_SIZE = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_DEFAULT_COUNT
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_KEY_TAG
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientHistoryListConverter(
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Converter<List<TxHistoryItem>, ImmutableList<DestinationRecipientListUM>> {
|
||||
|
||||
override fun convert(value: List<TxHistoryItem>): ImmutableList<DestinationRecipientListUM> {
|
||||
return value.filterRecipients(cryptoCurrency).ifEmpty {
|
||||
emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<TxHistoryItem>.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item ->
|
||||
val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer
|
||||
val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User
|
||||
val isSingleAddress = if (item.isOutgoing) {
|
||||
item.destinationType is TxHistoryItem.DestinationType.Single
|
||||
} else {
|
||||
item.sourceType is TxHistoryItem.SourceType.Single
|
||||
}
|
||||
val notZero = !item.amount.isZero()
|
||||
isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero
|
||||
}
|
||||
.take(RECENT_LIST_SIZE)
|
||||
.mapIndexed { index, tx ->
|
||||
DestinationRecipientListUM(
|
||||
id = "${RECENT_KEY_TAG}$index",
|
||||
title = tx.extractAddress(),
|
||||
subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()),
|
||||
timestamp = tx.extractTimestamp(),
|
||||
subtitleEndOffset = cryptoCurrency.symbol.length,
|
||||
subtitleIconRes = tx.extractIconRes(),
|
||||
)
|
||||
}.toPersistentList()
|
||||
|
||||
private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) {
|
||||
when (val destination = destinationType) {
|
||||
is TxHistoryItem.DestinationType.Multiple -> resourceReference(
|
||||
R.string.transaction_history_multiple_addresses,
|
||||
)
|
||||
is TxHistoryItem.DestinationType.Single -> stringReference(destination.addressType.address)
|
||||
}
|
||||
} else {
|
||||
when (val source = sourceType) {
|
||||
is TxHistoryItem.SourceType.Multiple -> resourceReference(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.SourceType.Single -> stringReference(source.address)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractIconRes() = if (isOutgoing) {
|
||||
R.drawable.ic_arrow_up_24
|
||||
} else {
|
||||
R.drawable.ic_arrow_down_24
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String {
|
||||
return amount.format { crypto(cryptoCurrency) }
|
||||
}
|
||||
|
||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||
val date = timestampInMillis.toDateFormatWithTodayYesterday(
|
||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||
)
|
||||
val time = timestampInMillis.toTimeFormat()
|
||||
return TextReference.Res(R.string.send_date_format, wrappedList(date, time))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RECENT_LIST_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.converters
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientWalletListConverter(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
) :
|
||||
Converter<List<DestinationWalletUM?>, PersistentList<DestinationRecipientListUM>> {
|
||||
override fun convert(value: List<DestinationWalletUM?>): PersistentList<DestinationRecipientListUM> {
|
||||
return value.filterWallets().ifEmpty {
|
||||
emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DestinationWalletUM?>.filterWallets(): PersistentList<DestinationRecipientListUM> {
|
||||
var walletsCounter = 0
|
||||
val currentAddress: String = runCatching {
|
||||
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||
}.getOrNull().orEmpty()
|
||||
|
||||
return this.filterNotNull()
|
||||
.filter {
|
||||
val isCoin = it.cryptoCurrency is CryptoCurrency.Coin
|
||||
val isNotSameAddress = it.address != currentAddress
|
||||
val isNotBlankAddress = it.address.isNotBlank()
|
||||
|
||||
isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable)
|
||||
}
|
||||
.groupBy { item -> item.name }
|
||||
.values.map { wallets ->
|
||||
val groupedByWallet = wallets.groupBy { it.userWalletId }
|
||||
var i = 0
|
||||
groupedByWallet
|
||||
.flatMap { item ->
|
||||
item.value.map { wallet ->
|
||||
val name = if (groupedByWallet.size > 1) {
|
||||
"${wallet.name} ${++i}"
|
||||
} else {
|
||||
wallet.name
|
||||
}
|
||||
|
||||
DestinationRecipientListUM(
|
||||
id = "${WALLET_KEY_TAG}${walletsCounter++}",
|
||||
title = stringReference(wallet.address),
|
||||
subtitle = stringReference(name),
|
||||
address = wallet.address,
|
||||
userWalletId = wallet.userWalletId,
|
||||
network = wallet.cryptoCurrency.network,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal const val WALLET_DEFAULT_COUNT = 1
|
||||
internal const val RECENT_DEFAULT_COUNT = 3
|
||||
internal const val WALLET_KEY_TAG = "wallet"
|
||||
internal const val RECENT_KEY_TAG = "recent"
|
||||
|
||||
internal fun loadingListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
DestinationRecipientListUM(
|
||||
id = "$tag$it",
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
internal fun emptyListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
DestinationRecipientListUM(
|
||||
id = "$tag$it",
|
||||
isLoading = false,
|
||||
isVisible = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationAddressTransformer(
|
||||
private val address: String,
|
||||
private val isPasted: Boolean,
|
||||
) : Transformer<DestinationUM> {
|
||||
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(
|
||||
addressTextField = state.addressTextField.copy(value = address, isValuePasted = isPasted),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationInitialStateTransformer(
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val onAddressChange: (String, EnterAddressSource) -> Unit,
|
||||
val onMemoChange: (String, Boolean) -> Unit,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) {
|
||||
Network.TransactionExtrasType.NONE -> null
|
||||
Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo
|
||||
Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field
|
||||
}
|
||||
return DestinationUM.Content(
|
||||
isPrimaryButtonEnabled = false,
|
||||
addressTextField = DestinationTextFieldUM.RecipientAddress(
|
||||
value = "",
|
||||
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
error = null,
|
||||
placeholder = resourceReference(R.string.send_enter_address_field),
|
||||
label = resourceReference(R.string.send_recipient),
|
||||
isValuePasted = false,
|
||||
),
|
||||
memoTextField = memoType?.let {
|
||||
DestinationTextFieldUM.RecipientMemo(
|
||||
value = "",
|
||||
onValueChange = { onMemoChange(it, false) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = resourceReference(R.string.send_optional_field),
|
||||
label = resourceReference(memoType),
|
||||
error = resourceReference(R.string.send_memo_destination_tag_error),
|
||||
disabledText = resourceReference(R.string.send_additional_field_already_included),
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
)
|
||||
},
|
||||
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
|
||||
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
|
||||
networkName = cryptoCurrencyStatus.currency.network.name,
|
||||
isValidating = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationMemoTransformer(
|
||||
private val memo: String,
|
||||
private val isPasted: Boolean,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
return state.copy(
|
||||
memoTextField = state.memoTextField?.copy(value = memo, isValuePasted = isPasted),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationRecentListTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val isUtxoConsolidationAvailable: Boolean,
|
||||
private val destinationWalletList: List<DestinationWalletUM>,
|
||||
private val txHistoryList: List<TxHistoryItem>,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(
|
||||
wallets = SendRecipientWalletListConverter(
|
||||
cryptoCurrencyStatus,
|
||||
isUtxoConsolidationAvailable,
|
||||
).convert(destinationWalletList),
|
||||
recent = SendRecipientHistoryListConverter(
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
).convert(txHistoryList),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.transaction.error.AddressValidation
|
||||
import com.tangem.domain.transaction.error.AddressValidationResult
|
||||
import com.tangem.domain.transaction.error.ValidateMemoError
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendDestinationValidationResultTransformer(
|
||||
private val addressValidationResult: AddressValidationResult,
|
||||
private val memoValidationResult: Either<ValidateMemoError, Unit>,
|
||||
) : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
val isValidAddress = addressValidationResult.isRight()
|
||||
val isValidMemo = memoValidationResult.isRight()
|
||||
|
||||
val addressErrorText = addressValidationResult.mapLeft {
|
||||
when (it) {
|
||||
is AddressValidation.Error.DataError,
|
||||
AddressValidation.Error.InvalidAddress,
|
||||
-> R.string.send_recipient_address_error
|
||||
AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet
|
||||
}
|
||||
}.leftOrNull()
|
||||
|
||||
return state.copy(
|
||||
isValidating = false,
|
||||
isPrimaryButtonEnabled = isValidAddress && isValidMemo,
|
||||
addressTextField = state.addressTextField.copy(
|
||||
error = addressErrorText?.let(::resourceReference),
|
||||
isError = state.addressTextField.value.isNotEmpty() && !isValidAddress,
|
||||
),
|
||||
memoTextField = state.memoTextField?.copy(
|
||||
isError = state.memoTextField.value.isNotEmpty() && !isValidMemo,
|
||||
isEnabled = !shouldDisableMemo(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** Ripple X-Address contains memo, so memo field is unnecessary */
|
||||
private fun shouldDisableMemo(): Boolean {
|
||||
return addressValidationResult.isRight { it == AddressValidation.Success.ValidXAddress }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal object SendDestinationValidationStartedTransformer : Transformer<DestinationUM> {
|
||||
override fun transform(prevState: DestinationUM): DestinationUM {
|
||||
val state = prevState as? DestinationUM.Content ?: return prevState
|
||||
|
||||
return state.copy(isValidating = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.rememberHapticFeedback
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
|
||||
/**
|
||||
* Row item with title and subtitle
|
||||
*
|
||||
* @param title title
|
||||
* @param subtitle subtitle
|
||||
* @param onClick click listener
|
||||
* @param modifier modifier
|
||||
* @param info info
|
||||
* @param subtitleEndOffset offset for subtitle ellipsis
|
||||
* @param subtitleIconRes icon
|
||||
*/
|
||||
@Composable
|
||||
fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
subtitleEndOffset: Int = 0,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = isLoading,
|
||||
label = "Recent List Content Animation",
|
||||
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
|
||||
) { isLoadingState ->
|
||||
if (isLoadingState) {
|
||||
ListItemLoading(modifier = modifier)
|
||||
} else {
|
||||
ListItemWithIcon(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
onClick = onClick,
|
||||
info = info,
|
||||
subtitleEndOffset = subtitleEndOffset,
|
||||
subtitleIconRes = subtitleIconRes,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListItemWithIcon(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
info: String? = null,
|
||||
subtitleEndOffset: Int = 0,
|
||||
@DrawableRes subtitleIconRes: Int? = null,
|
||||
) {
|
||||
val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { hapticFeedback() }
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
IdentIcon(
|
||||
address = title,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(20.dp)),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
EllipsisText(
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Justify,
|
||||
ellipsis = TextEllipsis.Middle,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Row {
|
||||
if (subtitleIconRes != null) {
|
||||
Icon(
|
||||
painter = painterResource(id = subtitleIconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(end = 2.dp)
|
||||
.size(16.dp)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.padding(2.dp),
|
||||
)
|
||||
}
|
||||
val (text, offset) = remember(subtitle, info) {
|
||||
if (info != null) {
|
||||
val suffix = ", $info"
|
||||
subtitle + suffix to suffix.length + subtitleEndOffset
|
||||
} else {
|
||||
subtitle to 0
|
||||
}
|
||||
}
|
||||
EllipsisText(
|
||||
text = text,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListItemLoading(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
) {
|
||||
CircleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.size(40.dp),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(36.dp)
|
||||
.padding(start = 12.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
radius = 3.dp,
|
||||
modifier = Modifier.size(
|
||||
width = 70.dp,
|
||||
height = 12.dp,
|
||||
),
|
||||
)
|
||||
RectangleShimmer(
|
||||
radius = 3.dp,
|
||||
modifier = Modifier.size(
|
||||
width = 52.dp,
|
||||
height = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ListItemWithIconPreview(
|
||||
@PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
ListItemWithIcon(
|
||||
title = config.title,
|
||||
subtitle = config.subtitle,
|
||||
subtitleEndOffset = config.subtitleEndOffset,
|
||||
subtitleIconRes = config.iconRes,
|
||||
onClick = {},
|
||||
isLoading = config.isLoading,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class ListItemWithIconPreviewConfig(
|
||||
val title: String,
|
||||
val subtitle: String,
|
||||
val info: String? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
val iconRes: Int? = null,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
||||
private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider<ListItemWithIconPreviewConfig>(
|
||||
collection = listOf(
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "1 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "Wallet",
|
||||
),
|
||||
ListItemWithIconPreviewConfig(
|
||||
title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
|
||||
subtitle = "0.000000000000000000000000000000 BTC",
|
||||
info = "0.0.0000 at 00:00",
|
||||
subtitleEndOffset = "BTC".length,
|
||||
iconRes = R.drawable.ic_arrow_down_24,
|
||||
isLoading = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.components.inputrow.InputRowRecipient
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY"
|
||||
private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY"
|
||||
|
||||
@Composable
|
||||
internal fun SendDestinationContent(
|
||||
state: DestinationUM,
|
||||
clickIntents: SendDestinationClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
if (state !is DestinationUM.Content) return
|
||||
val recipients = state.recent
|
||||
val wallets = state.wallets
|
||||
val memoField = state.memoTextField
|
||||
val address = state.addressTextField
|
||||
val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } }
|
||||
val isError by remember(address.isError) { derivedStateOf { address.isError } }
|
||||
LazyColumn(
|
||||
modifier = Modifier // Do not put fillMaxSize() in here
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
addressItem(
|
||||
address = address,
|
||||
networkName = state.networkName,
|
||||
isError = isError,
|
||||
isValidating = isValidating,
|
||||
onAddressChange = clickIntents::onRecipientAddressValueChange,
|
||||
)
|
||||
memoField(
|
||||
memoField = memoField,
|
||||
onMemoChange = clickIntents::onRecipientMemoValueChange,
|
||||
)
|
||||
listHeaderItem(
|
||||
titleRes = R.string.send_recipient_wallets_title,
|
||||
isVisible = wallets.isNotEmpty() && wallets.first().isVisible,
|
||||
isFirst = true,
|
||||
)
|
||||
listItem(
|
||||
list = wallets,
|
||||
clickIntents = clickIntents,
|
||||
isLast = recipients.any { !it.isVisible },
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
listHeaderItem(
|
||||
titleRes = R.string.send_recent_transactions,
|
||||
isVisible = recipients.isNotEmpty() && recipients.first().isVisible,
|
||||
isFirst = wallets.any { !it.isVisible },
|
||||
)
|
||||
listItem(
|
||||
list = recipients,
|
||||
clickIntents = clickIntents,
|
||||
isLast = true,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.addressItem(
|
||||
address: DestinationTextFieldUM.RecipientAddress,
|
||||
networkName: String,
|
||||
isError: Boolean,
|
||||
isValidating: Boolean,
|
||||
onAddressChange: (String, EnterAddressSource) -> Unit,
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
FooterContainer(
|
||||
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)),
|
||||
) {
|
||||
InputRowRecipient(
|
||||
value = address.value,
|
||||
title = address.label,
|
||||
placeholder = address.placeholder,
|
||||
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
|
||||
onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) },
|
||||
isError = isError,
|
||||
isLoading = isValidating,
|
||||
error = address.error,
|
||||
isValuePasted = address.isValuePasted,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.memoField(
|
||||
memoField: DestinationTextFieldUM.RecipientMemo?,
|
||||
onMemoChange: (String, Boolean) -> Unit,
|
||||
) {
|
||||
if (memoField != null) {
|
||||
item(key = MEMO_FIELD_KEY) {
|
||||
val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText
|
||||
TextFieldWithPaste(
|
||||
value = memoField.value,
|
||||
label = memoField.label,
|
||||
placeholder = placeholder,
|
||||
footer = resourceReference(R.string.send_recipient_memo_footer),
|
||||
onValueChange = { onMemoChange(it, false) },
|
||||
onPasteClick = { onMemoChange(it, true) },
|
||||
modifier = Modifier.padding(top = 20.dp),
|
||||
labelStyle = TangemTheme.typography.subtitle2,
|
||||
isError = memoField.isError,
|
||||
error = memoField.error,
|
||||
isReadOnly = !memoField.isEnabled,
|
||||
isValuePasted = memoField.isValuePasted,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) {
|
||||
item(key = titleRes) {
|
||||
AnimateRecentAppearance(isVisible) {
|
||||
val (topPadding, paddingFromTop) = if (isFirst) {
|
||||
20.dp to 12.dp
|
||||
} else {
|
||||
0.dp to 8.dp
|
||||
}
|
||||
val topRadius = if (isFirst) 16.dp else 0.dp
|
||||
Text(
|
||||
text = stringResourceSafe(titleRes),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = topPadding)
|
||||
.clip(
|
||||
RoundedCornerShape(
|
||||
topEnd = topRadius,
|
||||
topStart = topRadius,
|
||||
),
|
||||
)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(
|
||||
top = paddingFromTop,
|
||||
bottom = 12.dp,
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.listItem(
|
||||
list: ImmutableList<DestinationRecipientListUM>,
|
||||
clickIntents: SendDestinationClickIntents,
|
||||
isLast: Boolean,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
items(
|
||||
count = list.size,
|
||||
key = { list[it].id },
|
||||
contentType = { list[it]::class.java },
|
||||
) { index ->
|
||||
val item = list[index]
|
||||
val title = item.title.resolveReference()
|
||||
|
||||
if (item.isVisible) {
|
||||
ListItemWithIcon(
|
||||
title = title,
|
||||
subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||
info = item.timestamp?.resolveReference(),
|
||||
subtitleEndOffset = item.subtitleEndOffset,
|
||||
subtitleIconRes = item.subtitleIconRes,
|
||||
onClick = {
|
||||
clickIntents.onRecipientAddressValueChange(
|
||||
title,
|
||||
EnterAddressSource.RecentAddress,
|
||||
)
|
||||
},
|
||||
isLoading = item.isLoading,
|
||||
modifier = Modifier
|
||||
.animateItem()
|
||||
.then(
|
||||
if (isLast && index == list.lastIndex) {
|
||||
Modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = 16.dp,
|
||||
bottomEnd = 16.dp,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) {
|
||||
AnimatedContent(
|
||||
targetState = isVisible,
|
||||
label = "Item Appearance Animation",
|
||||
transitionSpec = {
|
||||
(slideInHorizontally() + fadeIn())
|
||||
.togetherWith(slideOutVertically() + fadeOut())
|
||||
},
|
||||
) {
|
||||
if (it) {
|
||||
content()
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment.Companion.CenterEnd
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.fields.SimpleTextField
|
||||
import com.tangem.core.ui.components.inputrow.inner.CrossIcon
|
||||
import com.tangem.core.ui.components.inputrow.inner.PasteButton
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
|
||||
@Composable
|
||||
internal fun TextFieldWithPaste(
|
||||
value: String,
|
||||
placeholder: TextReference,
|
||||
label: TextReference,
|
||||
onValueChange: (String) -> Unit,
|
||||
onPasteClick: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
footer: TextReference? = null,
|
||||
labelStyle: TextStyle = TangemTheme.typography.body2,
|
||||
error: TextReference? = null,
|
||||
isError: Boolean = false,
|
||||
isReadOnly: Boolean = false,
|
||||
isValuePasted: Boolean = false,
|
||||
) {
|
||||
val (title, color) = when {
|
||||
isError && error != null -> error to TangemTheme.colors.text.warning
|
||||
isReadOnly -> label to TangemTheme.colors.text.tertiary
|
||||
else -> label to TangemTheme.colors.text.secondary
|
||||
}
|
||||
val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled
|
||||
FooterContainer(modifier, footer) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.padding(end = 12.dp),
|
||||
) {
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = labelStyle,
|
||||
color = color,
|
||||
)
|
||||
SimpleTextField(
|
||||
value = value,
|
||||
placeholder = placeholder,
|
||||
placeholderColor = placeholderColor,
|
||||
onValueChange = onValueChange,
|
||||
readOnly = isReadOnly,
|
||||
isValuePasted = isValuePasted,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = !isReadOnly,
|
||||
label = "Animate read only status change",
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(CenterVertically),
|
||||
) {
|
||||
CrossIcon(
|
||||
onClick = onPasteClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = !isReadOnly,
|
||||
label = "Animate read only status change",
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(CenterEnd),
|
||||
) {
|
||||
PasteButton(
|
||||
isPasteButtonVisible = value.isBlank(),
|
||||
onClick = onPasteClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui.state
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
@Immutable
|
||||
internal data class DestinationRecipientListUM(
|
||||
val id: String,
|
||||
val title: TextReference = TextReference.Companion.EMPTY,
|
||||
val subtitle: TextReference = TextReference.Companion.EMPTY,
|
||||
val timestamp: TextReference? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
val isVisible: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
val userWalletId: UserWalletId? = null,
|
||||
val network: Network? = null,
|
||||
val address: String? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui.state
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class DestinationTextFieldUM {
|
||||
|
||||
/** Current value */
|
||||
abstract val value: String
|
||||
|
||||
/** Lambda be invoked when value is been changed */
|
||||
abstract val onValueChange: (String) -> Unit
|
||||
|
||||
/** Keyboard options */
|
||||
abstract val keyboardOptions: KeyboardOptions
|
||||
|
||||
data class RecipientAddress(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val isValuePasted: Boolean,
|
||||
) : DestinationTextFieldUM()
|
||||
|
||||
data class RecipientMemo(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val placeholder: TextReference,
|
||||
val label: TextReference,
|
||||
val isError: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val disabledText: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
val isValuePasted: Boolean,
|
||||
) : DestinationTextFieldUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class DestinationUM {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
data class Content(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val addressTextField: DestinationTextFieldUM.RecipientAddress,
|
||||
val memoTextField: DestinationTextFieldUM.RecipientMemo?,
|
||||
val recent: ImmutableList<DestinationRecipientListUM>,
|
||||
val wallets: ImmutableList<DestinationRecipientListUM>,
|
||||
val networkName: String,
|
||||
val isValidating: Boolean = false,
|
||||
val isEditingDisabled: Boolean = false,
|
||||
) : DestinationUM()
|
||||
|
||||
data class Empty(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
) : DestinationUM()
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.send.v2.subcomponents.destination.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
* Available wallet to send
|
||||
*
|
||||
* @property name wallet name
|
||||
* @property userWalletId wallet id
|
||||
* @property address blockchain address
|
||||
* @property cryptoCurrency selected crypto currency
|
||||
*/
|
||||
@Immutable
|
||||
data class DestinationWalletUM(
|
||||
val name: String,
|
||||
val userWalletId: UserWalletId,
|
||||
val address: String,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue