Updated on 2026-08-14
This commit is contained in:
parent
330ae73357
commit
e7efe7b76a
4636 changed files with 234864 additions and 63507 deletions
1
features/send/impl/.gitignore
vendored
Normal file
1
features/send/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
91
features/send/impl/build.gradle.kts
Normal file
91
features/send/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.send.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(deps.androidx.appCompat)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.material)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.lifecycle.compose)
|
||||
implementation(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.navigation)
|
||||
implementation(deps.compose.navigation.hilt)
|
||||
implementation(deps.compose.paging)
|
||||
implementation(deps.compose.constraintLayout)
|
||||
|
||||
/** Tangem SDKs */
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.txhistory)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.qrScanning)
|
||||
implementation(projects.domain.qrScanning.models)
|
||||
implementation(projects.domain.settings)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.qrScanning.api)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.send.impl.di
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.DefaultSendRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
||||
/**
|
||||
* DI module provides implementation of [SendRouter]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
internal object SendRouterModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
fun provideSendRouter(appRouter: AppRouter, urlOpener: UrlOpener): SendRouter {
|
||||
return DefaultSendRouter(appRouter, urlOpener)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.qrscanning.models.SourceType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.SendFragment
|
||||
|
||||
internal class DefaultSendRouter(
|
||||
private val router: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : InnerSendRouter {
|
||||
|
||||
override fun getEntryFragment(): Fragment = SendFragment.create()
|
||||
|
||||
override fun openUrl(url: String) {
|
||||
urlOpener.openUrl(url)
|
||||
}
|
||||
|
||||
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
|
||||
router.pop { isSuccess ->
|
||||
if (isSuccess) {
|
||||
router.push(
|
||||
AppRoute.CurrencyDetails(
|
||||
userWalletId = userWalletId,
|
||||
currency = currency,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun openQrCodeScanner(network: String) {
|
||||
router.push(
|
||||
AppRoute.QrScanning(
|
||||
source = SourceType.SEND,
|
||||
networkName = network,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.send.impl.navigation
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
||||
interface InnerSendRouter : SendRouter {
|
||||
|
||||
/** Open website by [url] */
|
||||
fun openUrl(url: String)
|
||||
|
||||
/** Open token details screen by [userWalletId] and [currency] */
|
||||
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency)
|
||||
|
||||
/** Open QR code scanner screen */
|
||||
fun openQrCodeScanner(network: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.tangem.features.send.impl.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
import com.tangem.features.send.impl.navigation.InnerSendRouter
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.ui.SendScreen
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Send fragment
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class SendFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
lateinit var router: SendRouter
|
||||
|
||||
@Inject
|
||||
lateinit var appRouter: AppRouter
|
||||
|
||||
@Inject
|
||||
lateinit var analyticsEventsHandler: AnalyticsEventHandler
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
private val innerSendRouter: InnerSendRouter
|
||||
get() = requireNotNull(router as? InnerSendRouter) {
|
||||
"innerSendRouter should be instance of InnerSendRouter"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
|
||||
val isEditingDisabled = arguments?.getString(AppRoute.Send.TRANSACTION_ID_KEY) != null
|
||||
viewModel.setRouter(
|
||||
innerSendRouter,
|
||||
StateRouter(
|
||||
appRouter = appRouter,
|
||||
isEditingDisabled = isEditingDisabled,
|
||||
analyticsEventsHandler = analyticsEventsHandler,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SendScreen(uiState, currentState.value)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Create send fragment instance */
|
||||
fun create(): SendFragment = SendFragment()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.features.send.impl.presentation.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.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.OnOffState
|
||||
|
||||
/**
|
||||
* Send screen analytics
|
||||
*/
|
||||
internal sealed class SendAnalyticEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(category = "Token / Send", event = event, params = params) {
|
||||
|
||||
/** Close button clicked */
|
||||
data class CloseButtonClicked(
|
||||
val source: SendScreenSource,
|
||||
val isFromSummary: Boolean,
|
||||
val isValid: Boolean,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Button - Close",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
"FromSummary" to if (isFromSummary) "Yes" else "No",
|
||||
"isValid" to if (isValid) "Yes" else "No",
|
||||
),
|
||||
)
|
||||
|
||||
// region Address
|
||||
/** Recipient address screen opened */
|
||||
data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened")
|
||||
|
||||
/** Address to send entered */
|
||||
data class AddressEntered(val source: EnterAddressSource, val isValid: Boolean) : SendAnalyticEvents(
|
||||
event = "Address Entered",
|
||||
params = mapOf(
|
||||
SOURCE to source.name,
|
||||
VALIDATION to if (isValid) "Success" else "Fail",
|
||||
),
|
||||
)
|
||||
|
||||
/** Qr Code button clicked */
|
||||
data object QrCodeButtonClicked : SendAnalyticEvents(event = "Button - QR Code")
|
||||
// endregion
|
||||
|
||||
// region Amount
|
||||
/** Amount screen opened */
|
||||
data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened")
|
||||
|
||||
/** Selected currency */
|
||||
data class SelectedCurrency(val type: SelectedCurrencyType) : SendAnalyticEvents(
|
||||
event = "Selected Currency",
|
||||
params = mapOf(TYPE to type.value),
|
||||
)
|
||||
|
||||
/** Max amount button clicked */
|
||||
data object MaxAmountButtonClicked : SendAnalyticEvents(event = "Max Amount Taped")
|
||||
// endregion
|
||||
|
||||
// region Fee
|
||||
/** Fee screen opened */
|
||||
data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened")
|
||||
|
||||
/** Selected fee (send after next screen opened) */
|
||||
data class SelectedFee(val feeType: AnalyticsParam.FeeType) : SendAnalyticEvents(
|
||||
event = "Fee Selected",
|
||||
params = mapOf("Fee Type" to feeType.value),
|
||||
)
|
||||
|
||||
/** Custom fee selected */
|
||||
data object CustomFeeButtonClicked : SendAnalyticEvents(event = "Custom Fee Clicked")
|
||||
|
||||
/** Custom fee edited */
|
||||
data object GasPriceInserter : SendAnalyticEvents(event = "Gas Price Inserted")
|
||||
|
||||
/** Subtract from amount selector switched (send after next screen opened) */
|
||||
data class SubtractFromAmount(val status: Boolean) : SendAnalyticEvents(
|
||||
event = "Subtract from Amount",
|
||||
params = mapOf("Status" to if (status) OnOffState.On.value else OnOffState.Off.value),
|
||||
)
|
||||
// endregion
|
||||
|
||||
// region Confirmation
|
||||
/** Confirmation screen opened */
|
||||
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
|
||||
|
||||
/** Screen reopened from confirmation screen */
|
||||
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
|
||||
event = "Screen Reopened",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
// endregion
|
||||
|
||||
// region Transaction Result
|
||||
/** Transaction send screen opened */
|
||||
data class TransactionScreenOpened(
|
||||
val token: String,
|
||||
val feeType: AnalyticsParam.FeeType,
|
||||
) : SendAnalyticEvents(
|
||||
event = "Transaction Sent Screen Opened",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
FEE_TYPE to feeType.value,
|
||||
),
|
||||
)
|
||||
|
||||
/** Share button clicked */
|
||||
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
|
||||
|
||||
/** Expore button clicked */
|
||||
data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
|
||||
|
||||
/** If not enough fee notification is present */
|
||||
data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SendAnalyticEvents(
|
||||
event = "Notice - Not Enough Fee",
|
||||
params = mapOf(TOKEN_PARAM to token, BLOCKCHAIN to blockchain),
|
||||
)
|
||||
|
||||
/** 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),
|
||||
)
|
||||
|
||||
data object NoticeFeeCoverage : SendAnalyticEvents(
|
||||
event = "Notice - Network Fee Coverage",
|
||||
)
|
||||
|
||||
/** If error occurs during send transactions */
|
||||
data class TransactionError(val token: String) : SendAnalyticEvents(
|
||||
event = "Error - Transaction Rejected",
|
||||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
// endregion
|
||||
}
|
||||
|
||||
internal enum class SendScreenSource {
|
||||
Address,
|
||||
Amount,
|
||||
Fee,
|
||||
Confirm,
|
||||
}
|
||||
|
||||
internal enum class EnterAddressSource {
|
||||
QRCode,
|
||||
PasteButton,
|
||||
RecentAddress,
|
||||
}
|
||||
|
||||
internal enum class SelectedCurrencyType(val value: String) {
|
||||
Token("Token"),
|
||||
AppCurrency("App Currency"),
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.features.send.impl.presentation.analytics.utils
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.analytics.SelectedCurrencyType
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendScreenSource
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class SendScreenAnalyticSender(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyProvider: Provider<CryptoCurrency>,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
fun send(prevScreen: SendUiStateType, state: SendUiState) {
|
||||
when (prevScreen) {
|
||||
SendUiStateType.Fee -> {
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
|
||||
feeSelectorState?.selectedFee?.let { selectedFee ->
|
||||
val isCustomFeeEdited = feeState.fee?.amount?.value != feeSelectorState.fees.normal.amount.value
|
||||
if (selectedFee == FeeType.Custom && isCustomFeeEdited) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.GasPriceInserter)
|
||||
}
|
||||
sendSelectedFeeAnalytics(feeSelectorState)
|
||||
}
|
||||
}
|
||||
SendUiStateType.Amount -> {
|
||||
val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return
|
||||
val isFiatSelected = amountState.amountTextField.isFiatValue
|
||||
val selectedCurrency = if (!isFiatSelected) {
|
||||
SelectedCurrencyType.Token
|
||||
} else {
|
||||
SelectedCurrencyType.AppCurrency
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.SelectedCurrency(selectedCurrency),
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun sendOnClose() {
|
||||
val routerState = stateRouterProvider().currentState.value
|
||||
val state = currentStateProvider()
|
||||
|
||||
val (source, isValid) = when (routerState.type) {
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.EditRecipient,
|
||||
-> SendScreenSource.Address to (state.editRecipientState?.isPrimaryButtonEnabled ?: false)
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.EditAmount,
|
||||
-> SendScreenSource.Amount to (state.editAmountState?.isPrimaryButtonEnabled ?: false)
|
||||
SendUiStateType.Fee,
|
||||
SendUiStateType.EditFee,
|
||||
-> SendScreenSource.Fee to (state.editFeeState?.isPrimaryButtonEnabled ?: false)
|
||||
else -> SendScreenSource.Confirm to true
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.CloseButtonClicked(
|
||||
source = source,
|
||||
isFromSummary = routerState.isFromConfirmation,
|
||||
isValid = isValid,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendTransaction() {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val cryptoCurrency = cryptoCurrencyProvider()
|
||||
val feeState = state.getFeeState(isEditState) ?: return
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return
|
||||
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val feeType = getSendTransactionFeeType(feeSelectorState)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.TransactionScreenOpened(
|
||||
token = cryptoCurrency.symbol,
|
||||
feeType = feeType,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
Basic.TransactionSent(
|
||||
sentFrom = AnalyticsParam.TxSentFrom.Send(
|
||||
blockchain = cryptoCurrency.network.name,
|
||||
token = cryptoCurrency.symbol,
|
||||
feeType = feeType,
|
||||
),
|
||||
memoType = getSendTransactionMemoType(recipientState.memoTextField),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendSelectedFeeAnalytics(feeSelectorState: FeeSelectorState.Content) {
|
||||
val type = getSendTransactionFeeType(feeSelectorState)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.SelectedFee(type))
|
||||
}
|
||||
|
||||
private fun getSendTransactionFeeType(feeSelectorState: FeeSelectorState.Content): AnalyticsParam.FeeType =
|
||||
when (feeSelectorState.fees) {
|
||||
is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed
|
||||
is TransactionFee.Choosable -> when (feeSelectorState.selectedFee) {
|
||||
FeeType.Slow -> AnalyticsParam.FeeType.Min
|
||||
FeeType.Market -> AnalyticsParam.FeeType.Normal
|
||||
FeeType.Fast -> AnalyticsParam.FeeType.Max
|
||||
FeeType.Custom -> AnalyticsParam.FeeType.Custom
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSendTransactionMemoType(
|
||||
recipientMemo: SendTextField.RecipientMemo?,
|
||||
): Basic.TransactionSent.MemoType {
|
||||
val memo = recipientMemo?.value
|
||||
return when {
|
||||
memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty
|
||||
memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full
|
||||
else -> Basic.TransactionSent.MemoType.Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
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
|
||||
*/
|
||||
@Immutable
|
||||
data class AvailableWallet(
|
||||
val name: String,
|
||||
val userWalletId: UserWalletId,
|
||||
val address: String,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.send.impl.presentation.domain
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
data class SendRecipientListContent(
|
||||
val id: String,
|
||||
val title: TextReference = TextReference.EMPTY,
|
||||
val subtitle: TextReference = TextReference.EMPTY,
|
||||
val timestamp: TextReference? = null,
|
||||
val subtitleEndOffset: Int = 0,
|
||||
@DrawableRes val subtitleIconRes: Int? = null,
|
||||
val isVisible: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.features.send.impl.R
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendAlertUM : AlertUM {
|
||||
|
||||
data class GenericError(
|
||||
override val title: TextReference? = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertUM() {
|
||||
override val message: TextReference = resourceReference(R.string.common_unknown_error)
|
||||
override val confirmButtonText: TextReference =
|
||||
resourceReference(id = R.string.common_support)
|
||||
}
|
||||
|
||||
data class FeeIncreased(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title)
|
||||
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
|
||||
}
|
||||
|
||||
data class FeeTooLow(
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference = resourceReference(id = R.string.send_alert_fee_too_low_text)
|
||||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||
}
|
||||
|
||||
data class FeeTooHigh(
|
||||
val times: String,
|
||||
override val onConfirmClick: () -> Unit,
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference? = null
|
||||
override val message: TextReference =
|
||||
resourceReference(id = R.string.send_alert_fee_too_high_text, wrappedList(times))
|
||||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||
}
|
||||
|
||||
data class FeeUnreachableError(
|
||||
override val onConfirmClick: (() -> Unit),
|
||||
) : SendAlertUM() {
|
||||
override val title: TextReference = resourceReference(R.string.send_fee_unreachable_error_title)
|
||||
override val message: TextReference = resourceReference(R.string.send_fee_unreachable_error_text)
|
||||
override val confirmButtonText = resourceReference(R.string.warning_button_refresh)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendEvent {
|
||||
|
||||
data class ShowSnackBar(val text: TextReference) : SendEvent()
|
||||
|
||||
data class ShowAlert(val alert: AlertUM) : SendEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeStateFactory
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce event state for [SendUiState]
|
||||
*
|
||||
* @param currentStateProvider [Provider] of [SendUiState]
|
||||
* @param clickIntents [SendClickIntents]
|
||||
* @param feeStateFactory [FeeStateFactory]
|
||||
*/
|
||||
internal class SendEventStateFactory(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val feeStateFactory: FeeStateFactory,
|
||||
) {
|
||||
private val transactionErrorAlertConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TransactionErrorAlertConverter(
|
||||
popBackStack = clickIntents::popBackStack,
|
||||
onFailedTxEmailClick = clickIntents::onFailedTxEmailClick,
|
||||
)
|
||||
}
|
||||
|
||||
fun onConsumeEventState(): SendUiState {
|
||||
return currentStateProvider().copy(event = consumedEvent())
|
||||
}
|
||||
|
||||
fun getSendTransactionErrorState(error: SendTransactionError?, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val event = error?.let {
|
||||
transactionErrorAlertConverter.convert(error)?.let {
|
||||
triggeredEvent<SendEvent>(SendEvent.ShowAlert(it), onConsume)
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
event = event ?: consumedEvent(),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState)
|
||||
val feeSelector = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val newFee = when (fee) {
|
||||
is TransactionFee.Single -> fee.normal
|
||||
is TransactionFee.Choosable -> {
|
||||
when (feeSelector.selectedFee) {
|
||||
FeeType.Slow -> fee.minimum
|
||||
FeeType.Market -> fee.normal
|
||||
FeeType.Fast -> fee.priority
|
||||
FeeType.Custom -> return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO
|
||||
val oldFeeValue = feeStateFactory.feeConverter.convert(feeSelector).amount.value ?: BigDecimal.ZERO
|
||||
val updateFeeState = feeStateFactory.onFeeOnLoadedState(fee)
|
||||
return if (newFeeValue > oldFeeValue) {
|
||||
updateFeeState.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(SendAlertUM.FeeIncreased(onConsume)),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
onFeeNotIncreased()
|
||||
updateFeeState
|
||||
}
|
||||
}
|
||||
|
||||
fun getFeeTooLowAlert(onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertUM.FeeTooLow(
|
||||
onConfirmClick = clickIntents::showSend,
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeTooHighAlert(diff: String, onConsume: () -> Unit): SendUiState {
|
||||
return currentStateProvider().copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertUM.FeeTooHigh(
|
||||
onConfirmClick = clickIntents::showSend,
|
||||
times = diff,
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getGenericErrorState(error: Throwable? = null, onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertUM.GenericError(
|
||||
onConfirmClick = { clickIntents.onFailedTxEmailClick(error?.localizedMessage.orEmpty()) },
|
||||
),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeUnreachableErrorState(onConsume: () -> Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
event = triggeredEvent(
|
||||
data = SendEvent.ShowAlert(
|
||||
SendAlertUM.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
|
||||
),
|
||||
onConsume = onConsume,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
|
||||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountParameters
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter
|
||||
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class SendStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val userWalletProvider: Provider<UserWallet>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val isTapHelpPreviewEnabledProvider: Provider<Boolean>,
|
||||
) {
|
||||
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AmountStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
iconStateConverter = iconStateConverter,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatusProvider()),
|
||||
)
|
||||
}
|
||||
private val recipientStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientStateConverter(
|
||||
clickIntents = clickIntents,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendFeeStateConverter(
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val confirmStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendConfirmStateConverter(
|
||||
isTapHelpPreviewEnabledProvider = isTapHelpPreviewEnabledProvider,
|
||||
)
|
||||
}
|
||||
private val sendSyncEditConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendSyncEditConverter(currentStateProvider = currentStateProvider)
|
||||
}
|
||||
// region UI states
|
||||
fun getInitialState(): SendUiState = SendUiState(
|
||||
clickIntents = clickIntents,
|
||||
event = consumedEvent(),
|
||||
isEditingDisabled = false,
|
||||
isBalanceHidden = false,
|
||||
cryptoCurrencyName = "",
|
||||
isSubtracted = false,
|
||||
amountState = AmountState.Empty(false),
|
||||
editAmountState = AmountState.Empty(false),
|
||||
)
|
||||
|
||||
fun getReadyState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = if (state.amountState is AmountState.Empty) {
|
||||
amountStateConverter.convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWalletProvider().name),
|
||||
value = "",
|
||||
),
|
||||
)
|
||||
} else {
|
||||
state.amountState
|
||||
}
|
||||
return state.copy(
|
||||
amountState = amountState,
|
||||
recipientState = state.recipientState
|
||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
sendState = confirmStateConverter.convert(Unit),
|
||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||
)
|
||||
}
|
||||
|
||||
fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val amountState = if (state.amountState is AmountState.Empty) {
|
||||
amountStateConverter.convert(
|
||||
AmountParameters(
|
||||
title = stringReference(userWalletProvider().name),
|
||||
value = amount,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
state.amountState
|
||||
}
|
||||
return state.copy(
|
||||
amountState = amountState,
|
||||
recipientState = state.recipientState
|
||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||
sendState = confirmStateConverter.convert(Unit),
|
||||
isEditingDisabled = true,
|
||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||
)
|
||||
}
|
||||
|
||||
fun syncEditStates(isFromEdit: Boolean) = sendSyncEditConverter.convert(isFromEdit)
|
||||
|
||||
fun getOnHideBalanceState(isBalanceHidden: Boolean): SendUiState {
|
||||
return currentStateProvider().copy(isBalanceHidden = isBalanceHidden)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region send
|
||||
fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val balance = cryptoCurrencyStatusProvider().value.amount ?: return state
|
||||
val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return state
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state
|
||||
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||
return state.copy(
|
||||
isSubtracted = checkFeeCoverage(
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = state.sendState?.reduceAmountBy,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
sendState = state.sendState?.copy(
|
||||
isSending = isSending,
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
|
||||
state = state,
|
||||
isSending = isSending,
|
||||
notifications = state.sendState.notifications,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getTransactionSendState(txData: TransactionData.Uncompiled, txUrl: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
isSuccess = true,
|
||||
showTapHelp = false,
|
||||
txUrl = txUrl,
|
||||
notifications = persistentListOf(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getSendNotificationState(notifications: ImmutableList<NotificationUM>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val reducedBy = sendState.reduceAmountBy.takeIf {
|
||||
notifications.none {
|
||||
it is NotificationUM.Error.ExistentialDeposit ||
|
||||
it is NotificationUM.Error.TransactionLimitError ||
|
||||
it is NotificationUM.Warning.HighFeeError
|
||||
}
|
||||
}
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled(
|
||||
state = state,
|
||||
isSending = sendState.isSending,
|
||||
notifications = notifications,
|
||||
),
|
||||
reduceAmountBy = reducedBy,
|
||||
notifications = notifications,
|
||||
showTapHelp = sendState.showTapHelp && notifications.isEmpty(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getHiddenTapHelpState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
return state.copy(
|
||||
sendState = sendState.copy(showTapHelp = false),
|
||||
)
|
||||
}
|
||||
|
||||
private fun isPrimaryButtonEnabled(
|
||||
state: SendUiState,
|
||||
isSending: Boolean,
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
): Boolean {
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return false
|
||||
val hasErrorNotifications = notifications.any { it is NotificationUM.Error }
|
||||
return !hasErrorNotifications && !isSending && feeState.feeSelectorState is FeeSelectorState.Content
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Ui states of the send screen
|
||||
*/
|
||||
@Immutable
|
||||
internal data class SendUiState(
|
||||
val clickIntents: SendClickIntents,
|
||||
val isEditingDisabled: Boolean,
|
||||
val cryptoCurrencyName: String,
|
||||
val amountState: AmountState,
|
||||
val recipientState: SendStates.RecipientState? = null,
|
||||
val feeState: SendStates.FeeState? = null,
|
||||
val sendState: SendStates.SendState? = null,
|
||||
val editAmountState: AmountState,
|
||||
val editRecipientState: SendStates.RecipientState? = null,
|
||||
val editFeeState: SendStates.FeeState? = null,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isSubtracted: Boolean,
|
||||
val event: StateEvent<SendEvent>,
|
||||
) {
|
||||
|
||||
fun getAmountState(isEditState: Boolean): AmountState {
|
||||
return if (isEditState) {
|
||||
editAmountState
|
||||
} else {
|
||||
amountState
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecipientState(isEditState: Boolean): SendStates.RecipientState? {
|
||||
return if (isEditState) {
|
||||
editRecipientState
|
||||
} else {
|
||||
recipientState
|
||||
}
|
||||
}
|
||||
|
||||
fun getFeeState(isEditState: Boolean): SendStates.FeeState? {
|
||||
return if (isEditState) {
|
||||
editFeeState
|
||||
} else {
|
||||
feeState
|
||||
}
|
||||
}
|
||||
|
||||
fun copyWrapped(
|
||||
isEditState: Boolean,
|
||||
amountState: AmountState = this.amountState,
|
||||
feeState: SendStates.FeeState? = this.feeState,
|
||||
recipientState: SendStates.RecipientState? = this.recipientState,
|
||||
sendState: SendStates.SendState? = this.sendState,
|
||||
): SendUiState = if (isEditState) {
|
||||
copy(
|
||||
editAmountState = amountState,
|
||||
editFeeState = feeState,
|
||||
editRecipientState = recipientState,
|
||||
sendState = sendState,
|
||||
)
|
||||
} else {
|
||||
copy(
|
||||
amountState = amountState,
|
||||
feeState = feeState,
|
||||
recipientState = recipientState,
|
||||
sendState = sendState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
internal sealed class SendStates {
|
||||
|
||||
abstract val type: SendUiStateType
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
/** Recipient state */
|
||||
@Stable
|
||||
data class RecipientState(
|
||||
override val type: SendUiStateType = SendUiStateType.Recipient,
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
val addressTextField: SendTextField.RecipientAddress,
|
||||
val memoTextField: SendTextField.RecipientMemo?,
|
||||
val recent: ImmutableList<SendRecipientListContent>,
|
||||
val wallets: ImmutableList<SendRecipientListContent>,
|
||||
val network: String,
|
||||
val isValidating: Boolean = false,
|
||||
) : SendStates()
|
||||
|
||||
/** Fee and speed state */
|
||||
@Stable
|
||||
data class FeeState(
|
||||
override val type: SendUiStateType = SendUiStateType.Fee,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
val feeSelectorState: FeeSelectorState,
|
||||
val fee: Fee?,
|
||||
val rate: BigDecimal?,
|
||||
val isFeeConvertibleToFiat: Boolean,
|
||||
val appCurrency: AppCurrency,
|
||||
val isFeeApproximate: Boolean,
|
||||
val isCustomSelected: Boolean,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
val isTronToken: Boolean,
|
||||
) : SendStates()
|
||||
|
||||
/** Send state */
|
||||
@Stable
|
||||
data class SendState(
|
||||
override val type: SendUiStateType = SendUiStateType.Send,
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
val isSending: Boolean,
|
||||
val isSuccess: Boolean,
|
||||
val transactionDate: Long,
|
||||
val txUrl: String,
|
||||
val ignoreAmountReduce: Boolean,
|
||||
val reduceAmountBy: BigDecimal?,
|
||||
val isFromConfirmation: Boolean,
|
||||
val showTapHelp: Boolean,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
) : SendStates()
|
||||
}
|
||||
|
||||
data class SendUiCurrentScreen(
|
||||
val type: SendUiStateType,
|
||||
val isFromConfirmation: Boolean,
|
||||
)
|
||||
|
||||
enum class SendUiStateType {
|
||||
None,
|
||||
Recipient,
|
||||
Amount,
|
||||
Fee,
|
||||
Send,
|
||||
EditAmount,
|
||||
EditRecipient,
|
||||
EditFee,
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.features.send.impl.presentation.state
|
||||
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class StateRouter(
|
||||
private val appRouter: AppRouter,
|
||||
private val analyticsEventsHandler: AnalyticsEventHandler,
|
||||
private val isEditingDisabled: Boolean,
|
||||
) {
|
||||
private var mutableCurrentState: MutableStateFlow<SendUiCurrentScreen> = MutableStateFlow(getInitState())
|
||||
|
||||
val currentState: StateFlow<SendUiCurrentScreen>
|
||||
get() = mutableCurrentState
|
||||
|
||||
val isEditState: Boolean
|
||||
get() = currentState.value.isFromConfirmation
|
||||
|
||||
fun clear() {
|
||||
mutableCurrentState.update { getInitState() }
|
||||
}
|
||||
|
||||
fun popBackStack() {
|
||||
appRouter.pop()
|
||||
}
|
||||
|
||||
fun onBackClick(isSuccess: Boolean = false) {
|
||||
val type = currentState.value.type
|
||||
when {
|
||||
isSuccess -> popBackStack()
|
||||
isEditingDisabled -> when (type) {
|
||||
SendUiStateType.EditFee -> showSend()
|
||||
else -> popBackStack()
|
||||
}
|
||||
else -> when (type) {
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
SendUiStateType.Fee -> showSend()
|
||||
SendUiStateType.Send -> showAmount()
|
||||
SendUiStateType.Recipient -> popBackStack()
|
||||
SendUiStateType.EditAmount -> showSend()
|
||||
SendUiStateType.EditRecipient -> showSend()
|
||||
SendUiStateType.EditFee -> showSend()
|
||||
else -> popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onNextClick() {
|
||||
when (currentState.value.type) {
|
||||
SendUiStateType.Recipient -> showAmount()
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Fee,
|
||||
SendUiStateType.EditAmount,
|
||||
SendUiStateType.EditRecipient,
|
||||
SendUiStateType.EditFee,
|
||||
-> showSend()
|
||||
SendUiStateType.Send -> onBackClick()
|
||||
|
||||
else -> popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPrevClick() {
|
||||
if (isEditingDisabled) {
|
||||
popBackStack()
|
||||
} else {
|
||||
when (currentState.value.type) {
|
||||
SendUiStateType.Amount -> showRecipient()
|
||||
else -> popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showAmount(isFromConfirmation: Boolean = false) {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.AmountScreenOpened)
|
||||
mutableCurrentState.update {
|
||||
if (isFromConfirmation) {
|
||||
SendUiCurrentScreen(SendUiStateType.EditAmount, true)
|
||||
} else {
|
||||
SendUiCurrentScreen(SendUiStateType.Amount, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showRecipient(isFromConfirmation: Boolean = false) {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.AddressScreenOpened)
|
||||
mutableCurrentState.update {
|
||||
if (isFromConfirmation) {
|
||||
SendUiCurrentScreen(SendUiStateType.EditRecipient, true)
|
||||
} else {
|
||||
SendUiCurrentScreen(SendUiStateType.Recipient, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showFee(isFromConfirmation: Boolean = false) {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.FeeScreenOpened)
|
||||
mutableCurrentState.update {
|
||||
if (isFromConfirmation) {
|
||||
SendUiCurrentScreen(SendUiStateType.EditFee, true)
|
||||
} else {
|
||||
SendUiCurrentScreen(SendUiStateType.Fee, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showSend() {
|
||||
analyticsEventsHandler.send(SendAnalyticEvents.ConfirmationScreenOpened)
|
||||
mutableCurrentState.update { SendUiCurrentScreen(SendUiStateType.Send, isFromConfirmation = false) }
|
||||
}
|
||||
|
||||
private fun getInitState() = if (isEditingDisabled) {
|
||||
SendUiCurrentScreen(
|
||||
type = SendUiStateType.None,
|
||||
isFromConfirmation = false,
|
||||
)
|
||||
} else {
|
||||
SendUiCurrentScreen(
|
||||
type = SendUiStateType.Recipient,
|
||||
isFromConfirmation = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
|
||||
import com.tangem.utils.Provider
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Factory to produce amount state for [SendUiState]
|
||||
*/
|
||||
internal class AmountStateFactory(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
|
||||
) {
|
||||
|
||||
private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountFieldChangeConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
minimumTransactionAmountProvider = minimumTransactionAmountProvider,
|
||||
)
|
||||
}
|
||||
private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountFieldMaxAmountConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
minimumTransactionAmountProvider = minimumTransactionAmountProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val amountCurrencyConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountCurrencyConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
private val amountPasteConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendAmountPastedTriggerDismissConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
)
|
||||
}
|
||||
private val amountReduceByConverter by lazy {
|
||||
SendAmountReduceByConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
minimumTransactionAmountProvider = minimumTransactionAmountProvider,
|
||||
)
|
||||
}
|
||||
private val amountReduceToConverter by lazy {
|
||||
SendAmountReduceToConverter(
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
currentStateProvider = currentStateProvider,
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
minimumTransactionAmountProvider = minimumTransactionAmountProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value)
|
||||
|
||||
fun getOnAmountReduceByState(reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal) =
|
||||
amountReduceByConverter.convert(
|
||||
AmountReduceByTransformer.ReduceByData(
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
),
|
||||
)
|
||||
|
||||
fun getOnAmountReduceToState(reduceAmountTo: BigDecimal) = amountReduceToConverter.convert(reduceAmountTo)
|
||||
|
||||
fun getOnMaxAmountClick(): SendUiState {
|
||||
return amountFieldMaxAmountConverter.convert(Unit)
|
||||
}
|
||||
|
||||
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
||||
|
||||
fun getOnAmountPastedTriggerDismiss() = amountPasteConverter.convert(false)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendAmountCurrencyConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Boolean, SendUiState> {
|
||||
|
||||
override fun convert(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
amountState = AmountCurrencyTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendAmountPastedTriggerDismissConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
) : Converter<Boolean, SendUiState> {
|
||||
|
||||
override fun convert(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
amountState = AmountPastedTriggerDismissTransformer().transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendAmountReduceByConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
|
||||
) : Converter<AmountReduceByTransformer.ReduceByData, SendUiState> {
|
||||
|
||||
override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState) ?: return state
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(
|
||||
reduceAmountBy = value.reduceAmountBy,
|
||||
),
|
||||
amountState = AmountReduceByTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
minimumTransactionAmount = minimumTransactionAmountProvider(),
|
||||
value = value,
|
||||
).transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.send.impl.presentation.state.amount
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SendAmountReduceToConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
|
||||
) : Converter<BigDecimal, SendUiState> {
|
||||
|
||||
override fun convert(value: BigDecimal): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState) ?: return state
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
amountState = AmountReduceToTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||
minimumTransactionAmount = minimumTransactionAmountProvider(),
|
||||
value = value,
|
||||
).transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.send.impl.presentation.state.common
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendSyncEditConverter(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
) : Converter<Boolean, SendUiState> {
|
||||
override fun convert(value: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return if (value) {
|
||||
state.copy(
|
||||
amountState = state.editAmountState,
|
||||
feeState = state.editFeeState,
|
||||
recipientState = state.editRecipientState,
|
||||
)
|
||||
} else {
|
||||
state.copy(
|
||||
editAmountState = state.amountState,
|
||||
editRecipientState = state.recipientState,
|
||||
editFeeState = state.feeState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.features.send.impl.presentation.state.confirm
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendConfirmStateConverter(
|
||||
private val isTapHelpPreviewEnabledProvider: Provider<Boolean>,
|
||||
) : Converter<Unit, SendStates.SendState> {
|
||||
override fun convert(value: Unit): SendStates.SendState {
|
||||
return SendStates.SendState(
|
||||
isPrimaryButtonEnabled = false,
|
||||
isSending = false,
|
||||
isSuccess = false,
|
||||
transactionDate = 0L,
|
||||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
isFromConfirmation = true,
|
||||
showTapHelp = isTapHelpPreviewEnabledProvider(),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
package com.tangem.features.send.impl.presentation.state.confirm
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchainsdk.utils.minimalAmount
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addMinimumAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase
|
||||
import com.tangem.domain.tokens.GetCurrencyCheckUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTezos
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class SendNotificationFactory(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val validateTransactionUseCase: ValidateTransactionUseCase,
|
||||
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
|
||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val isSubtractAvailableProvider: Provider<Boolean>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val userWalletId: UserWalletId,
|
||||
) {
|
||||
|
||||
fun create(): Flow<ImmutableList<NotificationUM>> = stateRouterProvider().currentState
|
||||
.filter { it.type == SendUiStateType.Send }
|
||||
.map {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val balance = cryptoCurrencyStatus.value.amount.orZero()
|
||||
val sendState = state.sendState ?: return@map persistentListOf()
|
||||
val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf()
|
||||
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf()
|
||||
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
|
||||
val feeValue = feeState.fee?.amount?.value.orZero()
|
||||
val reduceAmountBy = sendState.reduceAmountBy.orZero()
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isSubtractAvailableProvider(),
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val sendingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isSubtractAvailableProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
val feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error
|
||||
|
||||
val recipientAddress = state.recipientState?.addressTextField?.value
|
||||
val feeCurrencyBalanceAfterTransaction = getFeeCurrencyBalanceAfterTx(
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatusProvider(),
|
||||
sendingCurrencyStatus = cryptoCurrencyStatus,
|
||||
sendingAmount = sendingAmount,
|
||||
feeValue = feeValue,
|
||||
)
|
||||
val currencyCheck = getCurrencyCheckUseCase(
|
||||
userWalletId = userWalletId,
|
||||
currencyStatus = cryptoCurrencyStatus,
|
||||
amount = sendingAmount,
|
||||
fee = feeValue,
|
||||
recipientAddress = recipientAddress,
|
||||
feeCurrencyBalanceAfterTransaction = feeCurrencyBalanceAfterTransaction,
|
||||
)
|
||||
buildList {
|
||||
addErrorNotifications(
|
||||
feeError = feeError,
|
||||
sendingAmount = sendingAmount,
|
||||
feeValue = feeValue,
|
||||
currencyCheck = currencyCheck,
|
||||
)
|
||||
addWarningNotifications(
|
||||
amountState = amountState,
|
||||
feeState = feeState,
|
||||
sendState = sendState,
|
||||
sendingAmount = sendingAmount,
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
currencyCheck = currencyCheck,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun dismissNotificationState(clazz: Class<out NotificationUM>, isIgnored: Boolean = false): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val sendState = state.sendState ?: return state
|
||||
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
|
||||
val updatedNotifications = sendState.notifications.toMutableList()
|
||||
updatedNotifications.removeAll(notificationsToRemove)
|
||||
return state.copy(
|
||||
sendState = sendState.copy(
|
||||
ignoreAmountReduce = isIgnored,
|
||||
reduceAmountBy = if (isIgnored) null else sendState.reduceAmountBy,
|
||||
notifications = updatedNotifications.toImmutableList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFeeCurrencyBalanceAfterTx(
|
||||
feeCurrencyStatus: CryptoCurrencyStatus?,
|
||||
sendingCurrencyStatus: CryptoCurrencyStatus,
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
): BigDecimal? {
|
||||
val sendingCurrencyBalance = sendingCurrencyStatus.value as? CryptoCurrencyStatus.Loaded
|
||||
val feeCurrencyBalance = feeCurrencyStatus?.value as? CryptoCurrencyStatus.Loaded
|
||||
if (feeCurrencyStatus?.value !is CryptoCurrencyStatus.Loaded) return null
|
||||
return when {
|
||||
feeCurrencyStatus == sendingCurrencyStatus -> sendingCurrencyBalance?.let {
|
||||
it.amount - sendingAmount - feeValue
|
||||
}
|
||||
else -> feeCurrencyBalance?.let { it.amount - feeValue }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MutableList<NotificationUM>.addErrorNotifications(
|
||||
feeError: GetFeeError?,
|
||||
sendingAmount: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
currencyCheck: CryptoCurrencyCheck,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val currency = cryptoCurrencyStatusProvider().currency
|
||||
val currencyWarning = getBalanceNotEnoughForFeeWarningUseCase(
|
||||
fee = feeValue,
|
||||
userWalletId = userWalletId,
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = feeCryptoCurrencyStatusProvider() ?: cryptoCurrencyStatus,
|
||||
).getOrNull()
|
||||
|
||||
addFeeUnreachableNotification(
|
||||
tokenStatus = cryptoCurrencyStatus,
|
||||
coinStatus = feeCryptoCurrencyStatusProvider() ?: cryptoCurrencyStatus,
|
||||
feeError = feeError,
|
||||
onReload = clickIntents::feeReload,
|
||||
onClick = clickIntents::onTokenDetailsClick,
|
||||
)
|
||||
addExceedBalanceNotification(
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
isSubtractionAvailable = isSubtractAvailableProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
addExceedsBalanceNotification(
|
||||
cryptoCurrencyWarning = currencyWarning,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(currency.network.backendId),
|
||||
onClick = clickIntents::onTokenDetailsClick,
|
||||
onAnalyticsEvent = {
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeNotEnoughFee(
|
||||
token = cryptoCurrencyStatus.currency.symbol,
|
||||
blockchain = cryptoCurrencyStatus.currency.network.name,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
if (!BlockchainUtils.isCardano(currency.network.id.value)) {
|
||||
addDustWarningNotification(
|
||||
dustValue = currencyCheck.dustValue,
|
||||
feeValue = feeValue,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatusProvider(),
|
||||
)
|
||||
}
|
||||
addTransactionLimitErrorNotification(
|
||||
currencyCheck = currencyCheck,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatusProvider(),
|
||||
feeValue = feeValue,
|
||||
onReduceClick = clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
addReserveAmountErrorNotification(
|
||||
reserveAmount = currencyCheck.reserveAmount,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrency = currency,
|
||||
isAccountFunded = currencyCheck.isAccountFunded,
|
||||
)
|
||||
addMinimumAmountErrorNotification(
|
||||
minimumSendAmount = currencyCheck.minimumSendAmount,
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrency = currency,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun MutableList<NotificationUM>.addWarningNotifications(
|
||||
amountState: AmountState.Data,
|
||||
feeState: SendStates.FeeState,
|
||||
sendState: SendStates.SendState,
|
||||
sendingAmount: BigDecimal,
|
||||
isFeeCoverage: Boolean,
|
||||
currencyCheck: CryptoCurrencyCheck,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val currency = cryptoCurrencyStatus.currency
|
||||
val amountValue = amountState.amountTextField.cryptoAmount.value
|
||||
val validationError = amountValue?.let {
|
||||
validateTransactionUseCase(
|
||||
userWalletId = userWalletId,
|
||||
amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency),
|
||||
fee = feeState.fee,
|
||||
memo = null,
|
||||
destination = "",
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).leftOrNull()
|
||||
}
|
||||
|
||||
addRentExemptionNotification(
|
||||
rentWarning = currencyCheck.rentWarning,
|
||||
)
|
||||
|
||||
addExistentialWarningNotification(
|
||||
existentialDeposit = currencyCheck.existentialDeposit,
|
||||
feeAmount = feeState.fee?.amount?.value.orZero(),
|
||||
sendingAmount = sendingAmount,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
onReduceClick = clickIntents::onAmountReduceByClick,
|
||||
)
|
||||
addFeeCoverageNotification(
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
amountField = amountState.amountTextField,
|
||||
sendingValue = sendingAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
)
|
||||
addValidateTransactionNotifications(
|
||||
dustValue = currencyCheck.dustValue.orZero(),
|
||||
minAdaValue = (feeState.fee as? Fee.CardanoToken)?.minAdaValue,
|
||||
validationError = validationError,
|
||||
cryptoCurrency = currency,
|
||||
onReduceClick = clickIntents::onAmountReduceToClick,
|
||||
)
|
||||
|
||||
addHighFeeWarningNotification(
|
||||
amountState.amountTextField.cryptoAmount.value.orZero(),
|
||||
sendState.ignoreAmountReduce,
|
||||
)
|
||||
addTooHighNotification(feeState.feeSelectorState)
|
||||
addTooLowNotification(feeState)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addHighFeeWarningNotification(
|
||||
sendAmount: BigDecimal,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
|
||||
val threshold = Blockchain.Tezos.minimalAmount()
|
||||
val isTotalBalance = sendAmount >= balance && balance > threshold
|
||||
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
||||
add(
|
||||
NotificationUM.Warning.HighFeeError(
|
||||
currencyName = cryptoCurrencyStatus.currency.name,
|
||||
amount = threshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
clickIntents.onAmountReduceByClick(
|
||||
reduceAmountBy = threshold,
|
||||
reduceAmountByDiff = threshold,
|
||||
notification = NotificationUM.Warning.HighFeeError::class.java,
|
||||
)
|
||||
},
|
||||
onCloseClick = {
|
||||
clickIntents.onNotificationCancel(NotificationUM.Warning.HighFeeError::class.java)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooLowNotification(feeState: SendStates.FeeState) {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
if (feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue) {
|
||||
add(NotificationUM.Warning.FeeTooLow)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeTransactionDelays(
|
||||
cryptoCurrencyStatusProvider().currency.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||
if (feeSelectorState !is FeeSelectorState.Content) return
|
||||
|
||||
checkIfFeeTooHigh(feeSelectorState) { diff ->
|
||||
add(NotificationUM.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
/**
|
||||
* Check and calculates subtracted amount
|
||||
*/
|
||||
internal fun checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable: Boolean,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal,
|
||||
): BigDecimal {
|
||||
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||
val isFeeCoverage = checkFeeCoverage(
|
||||
isSubtractAvailable = isAmountSubtractAvailable,
|
||||
balance = balance,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
return if (isFeeCoverage) {
|
||||
balance.minus(reduceAmountBy).minus(feeValue)
|
||||
} else {
|
||||
amountValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sending amount with fee is greater than balance
|
||||
*/
|
||||
internal fun checkFeeCoverage(
|
||||
isSubtractAvailable: Boolean,
|
||||
balance: BigDecimal,
|
||||
amountValue: BigDecimal,
|
||||
feeValue: BigDecimal,
|
||||
reduceAmountBy: BigDecimal?,
|
||||
): Boolean {
|
||||
if (!isSubtractAvailable) return false
|
||||
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
|
||||
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too low
|
||||
*/
|
||||
internal fun checkIfFeeTooLow(feeSelectorState: FeeSelectorState.Content): Boolean {
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return false
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
|
||||
return feeSelectorState.selectedFee == FeeType.Custom && minimumValue > customValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if custom fee is too high
|
||||
*/
|
||||
internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onShow: (String) -> Unit): Boolean {
|
||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
||||
val highValue = multipleFees.priority.amount.value ?: return false
|
||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
val diff = if (highValue > BigDecimal.ZERO) {
|
||||
customValue / highValue
|
||||
} else {
|
||||
BigDecimal.ZERO
|
||||
}
|
||||
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF
|
||||
if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP))
|
||||
return isShow
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if fee exceeds fee paid currency balance
|
||||
*/
|
||||
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
|
||||
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
|
||||
}
|
||||
|
||||
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||
private const val ZERO_DECIMALS = 0
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.KaspaCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class FeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : Converter<FeeSelectorState.Content, Fee> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
KaspaCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: FeeSelectorState.Content): Fee {
|
||||
return when (val fees = value.fees) {
|
||||
is TransactionFee.Choosable -> {
|
||||
when (value.selectedFee) {
|
||||
FeeType.Slow -> fees.minimum
|
||||
FeeType.Market -> fees.normal
|
||||
FeeType.Fast -> fees.priority
|
||||
FeeType.Custom -> convertCustom(value, fees)
|
||||
}
|
||||
}
|
||||
is TransactionFee.Single ->
|
||||
when (value.selectedFee) {
|
||||
FeeType.Market -> fees.normal
|
||||
FeeType.Custom -> convertCustom(value, fees)
|
||||
else -> fees.normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertCustom(feeSelectorState: FeeSelectorState.Content, fees: TransactionFee): Fee {
|
||||
val customValues = feeSelectorState.customValues
|
||||
val normalFee = fees.normal
|
||||
return if (customValues.isEmpty()) {
|
||||
normalFee
|
||||
} else {
|
||||
when (normalFee) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues)
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues)
|
||||
else -> {
|
||||
val customFee = customValues.firstOrNull()
|
||||
Fee.Common(
|
||||
normalFee.amount.copy(
|
||||
value = customFee?.value?.parseToBigDecimal(customFee.decimals),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeeNotificationFactory(
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val clickIntents: SendClickIntents,
|
||||
) {
|
||||
|
||||
fun create() = stateRouterProvider().currentState
|
||||
.filter { it.type == SendUiStateType.Fee || it.type == SendUiStateType.EditFee }
|
||||
.map {
|
||||
val state = currentStateProvider()
|
||||
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return@map persistentListOf()
|
||||
buildList {
|
||||
addFeeUnreachableNotification(
|
||||
feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error,
|
||||
tokenName = state.cryptoCurrencyName,
|
||||
onReload = clickIntents::feeReload,
|
||||
)
|
||||
}.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Immutable
|
||||
internal sealed class FeeSelectorState {
|
||||
|
||||
data class Content(
|
||||
val fees: TransactionFee,
|
||||
val selectedFee: FeeType = FeeType.Market,
|
||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||
) : FeeSelectorState()
|
||||
|
||||
data object Loading : FeeSelectorState()
|
||||
|
||||
data class Error(
|
||||
val error: GetFeeError?,
|
||||
) : FeeSelectorState()
|
||||
}
|
||||
|
||||
enum class FeeType {
|
||||
Slow,
|
||||
Market,
|
||||
Fast,
|
||||
Custom,
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.blockchain.common.AmountType as SdkAmountType
|
||||
|
||||
/**
|
||||
* Factory to produce fee state for [SendUiState]
|
||||
*/
|
||||
internal class FeeStateFactory(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
|
||||
) {
|
||||
private val customFeeFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendFeeCustomFieldConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
val feeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
FeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadingState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) {
|
||||
feeState.feeSelectorState
|
||||
} else {
|
||||
FeeSelectorState.Loading
|
||||
},
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnLoadedState(fees: TransactionFee): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content
|
||||
|
||||
val isCustomWasSelected = if (feeState.isCustomSelected) {
|
||||
feeSelectorState?.customValues ?: persistentListOf()
|
||||
} else {
|
||||
customFeeFieldConverter.convert(fees.normal)
|
||||
}
|
||||
val updatedFeeSelectorState = feeSelectorState?.copy(
|
||||
fees = fees,
|
||||
customValues = isCustomWasSelected,
|
||||
) ?: FeeSelectorState.Content(
|
||||
fees = fees,
|
||||
customValues = customFeeFieldConverter.convert(fees.normal),
|
||||
)
|
||||
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(
|
||||
isPrimaryButtonEnabled = true,
|
||||
),
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
isFeeApproximate = isFeeApproximate(state.amountState),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeOnErrorState(feeError: GetFeeError?): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = state.getFeeState(isEditState)?.copy(
|
||||
feeSelectorState = FeeSelectorState.Error(feeError),
|
||||
),
|
||||
sendState = state.sendState?.copy(
|
||||
isPrimaryButtonEnabled = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onFeeSelectedState(feeType: FeeType): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
|
||||
val updatedFeeSelectorState = feeSelectorState.copy(selectedFee = feeType)
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
val isCustomFeeWasSelected = feeState.isCustomSelected || updatedFeeSelectorState.selectedFee == FeeType.Custom
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = feeState.copy(
|
||||
fee = fee,
|
||||
isCustomSelected = isCustomFeeWasSelected,
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
val updatedFeeSelectorState = customFeeFieldConverter.onValueChange(feeSelectorState, index, value)
|
||||
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getFeeNotificationState(notifications: ImmutableList<NotificationUM>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = feeState.copy(
|
||||
notifications = notifications,
|
||||
isPrimaryButtonEnabled = isPrimaryButtonEnabled(feeState, notifications),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun tryAutoFixCustomFeeValue(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val feeState = state.getFeeState(isEditState) ?: return state
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||
return when (feeSelectorState.selectedFee) {
|
||||
FeeType.Slow,
|
||||
FeeType.Market,
|
||||
FeeType.Fast,
|
||||
-> state
|
||||
FeeType.Custom -> {
|
||||
val updatedFeeSelectorState = customFeeFieldConverter.tryAutoFixValue(feeSelectorState)
|
||||
|
||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
feeState = feeState.copy(
|
||||
feeSelectorState = updatedFeeSelectorState,
|
||||
fee = fee,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPrimaryButtonEnabled(
|
||||
feeState: SendStates.FeeState,
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
): Boolean {
|
||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||
val customValue = feeSelectorState.customValues.firstOrNull()
|
||||
|
||||
val isNotCustom = feeSelectorState.selectedFee != FeeType.Custom
|
||||
val isNotEmptyCustom = if (customValue != null) {
|
||||
!customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val noErrors = notifications.none { it is NotificationUM.Error }
|
||||
|
||||
return noErrors && (isNotEmptyCustom || isNotCustom)
|
||||
}
|
||||
|
||||
private fun isFeeApproximate(state: AmountState): Boolean {
|
||||
val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false
|
||||
val amount = (state as? AmountState.Data)?.amountTextField?.cryptoAmount ?: return false
|
||||
return isFeeApproximateUseCase(
|
||||
networkId = cryptoCurrencyStatus.currency.network.id,
|
||||
amountType = amount.type.toSdkAmountType(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun AmountType.toSdkAmountType(): SdkAmountType {
|
||||
return when (this) {
|
||||
AmountType.CoinType -> SdkAmountType.Coin
|
||||
is AmountType.FiatType -> error("unsupported type FiatType")
|
||||
AmountType.ReserveType -> SdkAmountType.Reserve
|
||||
is AmountType.TokenType -> SdkAmountType.Token(
|
||||
Token(
|
||||
name = this.token.name,
|
||||
symbol = this.token.symbol,
|
||||
contractAddress = this.token.contractAddress,
|
||||
decimals = this.token.decimals,
|
||||
id = this.token.id.rawCurrencyId?.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.KaspaCustomFeeConverter
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendFeeCustomFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : Converter<Fee, ImmutableList<SendTextField.CustomFee>> {
|
||||
|
||||
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EthereumCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BitcoinCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
stateRouterProvider = stateRouterProvider,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
KaspaCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Fee): ImmutableList<SendTextField.CustomFee> {
|
||||
return when (value) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value)
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value)
|
||||
else -> persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun onValueChange(feeSelectorState: FeeSelectorState.Content, index: Int, value: String) = feeSelectorState.copy(
|
||||
customValues = when (val fee = feeSelectorState.fees.normal) {
|
||||
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
|
||||
feeValue = fee,
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
txSize = fee.txSize,
|
||||
)
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
|
||||
customValues = feeSelectorState.customValues,
|
||||
index = index,
|
||||
value = value,
|
||||
)
|
||||
else -> feeSelectorState.customValues
|
||||
},
|
||||
)
|
||||
|
||||
fun tryAutoFixValue(feeSelectorState: FeeSelectorState.Content) = feeSelectorState.copy(
|
||||
customValues = when (feeSelectorState.fees) {
|
||||
is TransactionFee.Choosable -> feeSelectorState.fees.minimum
|
||||
is TransactionFee.Single -> feeSelectorState.fees.normal
|
||||
}.let {
|
||||
when (it) {
|
||||
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
|
||||
minimumFee = it,
|
||||
customValues = feeSelectorState.customValues,
|
||||
)
|
||||
else -> feeSelectorState.customValues
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isTron
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendFeeStateConverter(
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<Unit, SendStates.FeeState> {
|
||||
|
||||
override fun convert(value: Unit): SendStates.FeeState {
|
||||
return SendStates.FeeState(
|
||||
feeSelectorState = FeeSelectorState.Loading,
|
||||
fee = null,
|
||||
notifications = persistentListOf(),
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
isFeeApproximate = false,
|
||||
isCustomSelected = false,
|
||||
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
|
||||
isTronToken = cryptoCurrencyStatusProvider().currency is CryptoCurrency.Token &&
|
||||
isTron(cryptoCurrencyStatusProvider().currency.network.id.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Base ethereum custom fee converter
|
||||
*
|
||||
* @param T subtype of [Fee.Ethereum]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface BaseEthereumCustomFeeConverter<T : Fee.Ethereum> : CustomFeeConverter<T> {
|
||||
|
||||
fun getGasLimitIndex(feeValue: T): Int
|
||||
|
||||
fun onValueChange(
|
||||
feeValue: T,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<SendTextField.CustomFee>
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isUseBitcoinFeeConverter
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class BitcoinCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : CustomFeeConverter<Fee.Bitcoin> {
|
||||
|
||||
override fun convert(value: Fee.Bitcoin): ImmutableList<SendTextField.CustomFee> {
|
||||
val feeValue = value.amount.value
|
||||
val feeCurrency = feeCryptoCurrencyStatusProvider()?.value
|
||||
val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value
|
||||
return if (network != null && isUseBitcoinFeeConverter(network)) {
|
||||
persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_bitcoin_custom_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = feeCurrency?.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
isReadonly = true,
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = toSatoshiPerByte(
|
||||
amount = feeValue,
|
||||
decimals = value.amount.decimals,
|
||||
txSize = value.txSize,
|
||||
).toString(),
|
||||
decimals = SATOSHI_DECIMALS,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_satoshi_per_byte_title),
|
||||
footer = resourceReference(R.string.send_satoshi_per_byte_text),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (checkExceedBalance(
|
||||
feeBalance = feeCurrency?.amount,
|
||||
feeAmount = feeValue,
|
||||
)
|
||||
) {
|
||||
ImeAction.None
|
||||
} else {
|
||||
ImeAction.Done
|
||||
},
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) },
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList<SendTextField.CustomFee>): Fee.Bitcoin {
|
||||
val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals)
|
||||
val satoshiPerByte = value[FEE_SATOSHI_INDEX].value.parseToBigDecimal(value[FEE_SATOSHI_INDEX].decimals)
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
satoshiPerByte = satoshiPerByte,
|
||||
)
|
||||
}
|
||||
|
||||
fun onValueChange(
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
txSize: BigDecimal,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
if (index == FEE_SATOSHI_INDEX) {
|
||||
val newSatoshiPerKb = value.parseToBigDecimal(this[FEE_SATOSHI_INDEX].decimals)
|
||||
val newFeeAmount = newSatoshiPerKb.multiply(txSize)
|
||||
.movePointLeft(this[FEE_AMOUNT_INDEX].decimals)
|
||||
.setScale(this[FEE_AMOUNT_INDEX].decimals, RoundingMode.DOWN)
|
||||
set(
|
||||
FEE_AMOUNT_INDEX,
|
||||
this[FEE_AMOUNT_INDEX].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? {
|
||||
val newFeeAmount = amount?.movePointRight(decimals)
|
||||
return newFeeAmount?.divide(
|
||||
txSize,
|
||||
SATOSHI_DECIMALS,
|
||||
RoundingMode.HALF_UP,
|
||||
)?.setScale(SATOSHI_DECIMALS, RoundingMode.HALF_UP)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FEE_AMOUNT_INDEX = 0
|
||||
private const val FEE_SATOSHI_INDEX = 1
|
||||
private const val SATOSHI_DECIMALS = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal interface CustomFeeConverter<T : Fee> : Converter<T, ImmutableList<SendTextField.CustomFee>> {
|
||||
fun convertBack(normalFee: T, value: ImmutableList<SendTextField.CustomFee>): T
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class EthereumCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum> {
|
||||
|
||||
private val feeCurrency: CryptoCurrencyStatus.Value?
|
||||
get() = feeCryptoCurrencyStatusProvider()?.value
|
||||
|
||||
private val legacyFeeConverter = EthereumLegacyCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
|
||||
private val eipFeeConverter = EthereumEIPCustomFeeConverter(
|
||||
clickIntents = clickIntents,
|
||||
appCurrencyProvider = appCurrencyProvider,
|
||||
feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider,
|
||||
)
|
||||
|
||||
override fun convert(value: Fee.Ethereum): ImmutableList<SendTextField.CustomFee> {
|
||||
return buildList {
|
||||
convertFeeValue(value).let(::add)
|
||||
|
||||
when (value) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value)
|
||||
}
|
||||
.let(::addAll)
|
||||
|
||||
convertGasLimitValue(value).let(::add)
|
||||
}
|
||||
.toImmutableList()
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList<SendTextField.CustomFee>): Fee.Ethereum {
|
||||
return when (normalFee) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.convertBack(normalFee = normalFee, value = value)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum): Int {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.getGasLimitIndex(feeValue)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.getGasLimitIndex(feeValue)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
return when (feeValue) {
|
||||
is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(feeValue, customValues, index, value)
|
||||
is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange(feeValue, customValues, index, value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertFeeValue(value: Fee.Ethereum): SendTextField.CustomFee {
|
||||
val feeValue = value.amount.value
|
||||
|
||||
return SendTextField.CustomFee(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = feeCurrency?.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertGasLimitValue(value: Fee.Ethereum): SendTextField.CustomFee {
|
||||
val isExceedBalance = checkExceedBalance(feeBalance = feeCurrency?.amount, feeAmount = value.amount.value)
|
||||
|
||||
return SendTextField.CustomFee(
|
||||
value = value.gasLimit.toString(),
|
||||
decimals = GAS_DECIMALS,
|
||||
symbol = "",
|
||||
title = resourceReference(R.string.send_gas_limit),
|
||||
footer = resourceReference(R.string.send_gas_limit_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(getGasLimitIndex(value), it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (isExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ETHEREUM_GAS_UNIT = "GWEI"
|
||||
const val GIGA_DECIMALS = 9
|
||||
const val GAS_DECIMALS = 0
|
||||
const val FEE_AMOUNT = 0
|
||||
}
|
||||
}
|
||||
|
||||
internal fun MutableList<SendTextField.CustomFee>.setEmpty(index: Int) {
|
||||
set(index, this[index].copy(value = ""))
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.GAS_DECIMALS
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class EthereumEIPCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.EIP1559> {
|
||||
|
||||
override fun convert(value: Fee.Ethereum.EIP1559): ImmutableList<SendTextField.CustomFee> {
|
||||
return persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = value.maxFeePerGas.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_custom_evm_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_evm_max_fee_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(MAX_FEE, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
SendTextField.CustomFee(
|
||||
value = value.priorityFee.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_custom_evm_priority_fee),
|
||||
footer = resourceReference(R.string.send_custom_evm_priority_fee_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(PRIORITY_FEE, it) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(
|
||||
normalFee: Fee.Ethereum.EIP1559,
|
||||
value: ImmutableList<SendTextField.CustomFee>,
|
||||
): Fee.Ethereum.EIP1559 {
|
||||
val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals)
|
||||
val maxFeeDecimals = value[MAX_FEE].decimals
|
||||
val maxFee = value[MAX_FEE].value.parseToBigDecimal(maxFeeDecimals)
|
||||
.movePointRight(maxFeeDecimals)
|
||||
.toBigInteger()
|
||||
val priorityFeeDecimals = value[PRIORITY_FEE].decimals
|
||||
val priorityFee = value[PRIORITY_FEE].value.parseToBigDecimal(priorityFeeDecimals)
|
||||
.movePointRight(priorityFeeDecimals)
|
||||
.toBigInteger()
|
||||
val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
maxFeePerGas = maxFee,
|
||||
priorityFee = priorityFee,
|
||||
gasLimit = gasLimit,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum.EIP1559): Int = GAS_LIMIT
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum.EIP1559,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT -> setOnAmountChange(value, index)
|
||||
MAX_FEE -> setOnMaxFeeChange(value, index)
|
||||
GAS_LIMIT -> setOnGasLimitChange(value, index)
|
||||
else -> set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnAmountChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(MAX_FEE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(GIGA_DECIMALS) // from ETH to GWEI
|
||||
|
||||
val newMaxFee = newFeeAmount.divide(gasLimit, this[MAX_FEE].decimals, RoundingMode.HALF_UP)
|
||||
|
||||
set(
|
||||
index = MAX_FEE,
|
||||
element = this[MAX_FEE].copy(value = newMaxFee.parseBigDecimal(this[MAX_FEE].decimals)),
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmountDecimal,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnMaxFeeChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(MAX_FEE)
|
||||
} else {
|
||||
val newMaxFee = value.parseToBigDecimal(this[MAX_FEE].decimals).movePointLeft(this[MAX_FEE].decimals)
|
||||
val newFeeAmount = gasLimit * newMaxFee
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnGasLimitChange(value: String, index: Int) {
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_LIMIT)
|
||||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
|
||||
val maxFee = this[MAX_FEE].value.parseToBigDecimal(this[MAX_FEE].decimals)
|
||||
.movePointLeft(this[MAX_FEE].decimals) // from GWEI to ETH
|
||||
|
||||
val newFeeAmount = newGasLimit * maxFee
|
||||
|
||||
set(
|
||||
index = FEE_AMOUNT,
|
||||
element = this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val isNotExceedBalance = checkExceedBalance(
|
||||
feeBalance = feeCryptoCurrencyStatusProvider()?.value?.amount,
|
||||
feeAmount = newFeeAmount,
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_FEE = 1
|
||||
const val PRIORITY_FEE = 2
|
||||
const val GAS_LIMIT = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.GAS_DECIMALS
|
||||
import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class EthereumLegacyCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.Legacy> {
|
||||
|
||||
override fun convert(value: Fee.Ethereum.Legacy): ImmutableList<SendTextField.CustomFee> {
|
||||
return persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS),
|
||||
decimals = GIGA_DECIMALS,
|
||||
symbol = ETHEREUM_GAS_UNIT,
|
||||
title = resourceReference(R.string.send_gas_price),
|
||||
footer = resourceReference(R.string.send_gas_price_footer),
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun convertBack(
|
||||
normalFee: Fee.Ethereum.Legacy,
|
||||
value: ImmutableList<SendTextField.CustomFee>,
|
||||
): Fee.Ethereum.Legacy {
|
||||
val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals)
|
||||
val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger()
|
||||
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
gasPrice = gasPrice,
|
||||
gasLimit = gasLimit,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getGasLimitIndex(feeValue: Fee.Ethereum.Legacy): Int = GAS_LIMIT
|
||||
|
||||
override fun onValueChange(
|
||||
feeValue: Fee.Ethereum.Legacy,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT -> setOnAmountChange(value, index)
|
||||
GAS_PRICE -> setOnGasPriceChange(value, index)
|
||||
GAS_LIMIT -> setOnGasLimitChange(value, index)
|
||||
else -> set(index, this[index].copy(value = value))
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnAmountChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
|
||||
val newFeeAmount = newFeeAmountDecimal.movePointRight(this[GAS_PRICE].decimals) // from ETH to GWEI
|
||||
val newGasPrice = newFeeAmount.divide(gasLimit, this[GAS_PRICE].decimals, RoundingMode.HALF_UP)
|
||||
set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(this[GAS_PRICE].decimals)))
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmountDecimal,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnGasPriceChange(value: String, index: Int) {
|
||||
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_PRICE)
|
||||
} else {
|
||||
val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
val newFeeAmount = gasLimit * newGasPrice
|
||||
set(
|
||||
FEE_AMOUNT,
|
||||
this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
set(index, this[index].copy(value = value))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<SendTextField.CustomFee>.setOnGasLimitChange(value: String, index: Int) {
|
||||
if (value.isBlank()) {
|
||||
setEmpty(FEE_AMOUNT)
|
||||
setEmpty(GAS_LIMIT)
|
||||
} else {
|
||||
val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals)
|
||||
val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals)
|
||||
.movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH
|
||||
|
||||
val newFeeAmount = newGasLimit * gasPrice
|
||||
|
||||
set(
|
||||
index = FEE_AMOUNT,
|
||||
element = this[FEE_AMOUNT].copy(
|
||||
value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals),
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = newFeeAmount,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val isNotExceedBalance = checkExceedBalance(
|
||||
feeBalance = feeCryptoCurrencyStatusProvider()?.value?.amount,
|
||||
feeAmount = newFeeAmount,
|
||||
)
|
||||
|
||||
set(
|
||||
index = index,
|
||||
element = this[index].copy(
|
||||
value = value,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val GAS_PRICE = 1
|
||||
const val GAS_LIMIT = 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fee.custom
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class KaspaCustomFeeConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val feeCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
||||
) : CustomFeeConverter<Fee.Kaspa> {
|
||||
|
||||
override fun convert(value: Fee.Kaspa): ImmutableList<SendTextField.CustomFee> {
|
||||
val feeValue = value.amount.value
|
||||
val feeCurrency = feeCryptoCurrencyStatusProvider()?.value
|
||||
val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value
|
||||
return if (network != null) {
|
||||
persistentListOf(
|
||||
SendTextField.CustomFee(
|
||||
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
|
||||
decimals = value.amount.decimals,
|
||||
symbol = value.amount.currencySymbol,
|
||||
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
title = resourceReference(R.string.send_max_fee),
|
||||
footer = resourceReference(R.string.send_custom_amount_fee_footer),
|
||||
label = getFiatReference(
|
||||
rate = feeCurrency?.fiatRate,
|
||||
value = feeValue,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
keyboardActions = KeyboardActions(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun convertBack(normalFee: Fee.Kaspa, value: ImmutableList<SendTextField.CustomFee>): Fee.Kaspa {
|
||||
val decimals = value[FEE_AMOUNT_INDEX].decimals
|
||||
val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(decimals)
|
||||
return normalFee.copy(
|
||||
amount = normalFee.amount.copy(value = feeAmount),
|
||||
mass = normalFee.mass,
|
||||
feeRate = feeAmount
|
||||
.divide(normalFee.mass.toBigDecimal(), decimals, RoundingMode.HALF_UP)
|
||||
.movePointRight(decimals)
|
||||
.toBigInteger(),
|
||||
)
|
||||
}
|
||||
|
||||
fun onValueChange(
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
index: Int,
|
||||
value: String,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
return mutableCustomValues.apply {
|
||||
when (index) {
|
||||
FEE_AMOUNT_INDEX -> {
|
||||
val valueDecimal = value.parseToBigDecimal(this[FEE_AMOUNT_INDEX].decimals)
|
||||
set(
|
||||
index,
|
||||
this[index].copy(
|
||||
value = value,
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = valueDecimal,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
fun tryAutoFixValue(
|
||||
minimumFee: Fee.Kaspa,
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
): ImmutableList<SendTextField.CustomFee> {
|
||||
val mutableCustomValues = customValues.toMutableList()
|
||||
val minimumFeeAmountValue = minimumFee.amount.value
|
||||
|
||||
return mutableCustomValues.apply {
|
||||
// check that there is reveal transaction info (= krc-20 token transfer)
|
||||
// return without changes otherwise
|
||||
if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) {
|
||||
getOrNull(FEE_AMOUNT_INDEX)?.let {
|
||||
val valueDecimal = it.value.parseToBigDecimal(it.decimals)
|
||||
// krc-20 transaction will be failed if custom fee value is less than minimum,
|
||||
// so we set value to minimum in this case
|
||||
if (valueDecimal < minimumFee.amount.value) {
|
||||
val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals)
|
||||
set(
|
||||
FEE_AMOUNT_INDEX,
|
||||
it.copy(
|
||||
value = fixedValue,
|
||||
label = getFiatReference(
|
||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||
value = valueDecimal,
|
||||
appCurrency = appCurrencyProvider(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val FEE_AMOUNT_INDEX = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendAmountFieldChangeConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
|
||||
) : Converter<String, SendUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun convert(value: String): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState)
|
||||
|
||||
val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
val minimumTransactionAmount = minimumTransactionAmountProvider()
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(reduceAmountBy = null),
|
||||
amountState = AmountFieldChangeTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxEnterAmount = maxEnterAmount,
|
||||
minimumTransactionAmount = minimumTransactionAmount,
|
||||
value = value,
|
||||
).transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter
|
||||
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer
|
||||
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.isNullOrZero
|
||||
|
||||
internal class SendAmountFieldMaxAmountConverter(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val minimumTransactionAmountProvider: Provider<EnterAmountBoundary?>,
|
||||
) : Converter<Unit, SendUiState> {
|
||||
|
||||
private val maxEnterAmountConverter = MaxEnterAmountConverter()
|
||||
|
||||
override fun convert(value: Unit): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val amountState = state.getAmountState(isEditState) ?: return state
|
||||
|
||||
val decimalCryptoValue = cryptoCurrencyStatus.value.amount
|
||||
if (decimalCryptoValue.isNullOrZero()) return state
|
||||
|
||||
val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus)
|
||||
val minimumTransactionAmount = minimumTransactionAmountProvider()
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
sendState = state.sendState?.copy(reduceAmountBy = null),
|
||||
amountState = AmountFieldSetMaxAmountTransformer(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
maxAmount = maxEnterAmount,
|
||||
minAmount = minimumTransactionAmount,
|
||||
).transform(amountState),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.features.send.impl.presentation.state.fields
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal sealed class SendTextField {
|
||||
|
||||
/** 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,
|
||||
) : SendTextField()
|
||||
|
||||
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,
|
||||
) : SendTextField()
|
||||
|
||||
data class CustomFee(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
val keyboardActions: KeyboardActions,
|
||||
val symbol: String?,
|
||||
val decimals: Int,
|
||||
val title: TextReference,
|
||||
val footer: TextReference,
|
||||
val label: TextReference? = null,
|
||||
val isReadonly: Boolean = false,
|
||||
) : SendTextField()
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object ConfirmStatePreviewData {
|
||||
|
||||
val sendState = SendStates.SendState(
|
||||
type = SendUiStateType.Send,
|
||||
isSending = false,
|
||||
isSuccess = false,
|
||||
transactionDate = 0L,
|
||||
txUrl = "",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
isFromConfirmation = false,
|
||||
showTapHelp = true,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
|
||||
val sendDoneState = SendStates.SendState(
|
||||
type = SendUiStateType.Send,
|
||||
isSending = false,
|
||||
isSuccess = true,
|
||||
transactionDate = 1695199500000L,
|
||||
txUrl = "url",
|
||||
ignoreAmountReduce = false,
|
||||
reduceAmountBy = null,
|
||||
isFromConfirmation = false,
|
||||
showTapHelp = false,
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType.Coin
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal object FeeStatePreviewData {
|
||||
|
||||
private val fee = Fee.Common(
|
||||
amount = Amount(
|
||||
currencySymbol = "MATIC",
|
||||
value = BigDecimal(0.159806),
|
||||
decimals = 18,
|
||||
type = Coin,
|
||||
),
|
||||
)
|
||||
|
||||
private val singleFee = TransactionFee.Single(normal = fee)
|
||||
|
||||
private val multipleFees = TransactionFee.Choosable(
|
||||
normal = fee,
|
||||
minimum = fee,
|
||||
priority = fee.copy(fee.amount.copy(value = BigDecimal(0.159824))),
|
||||
)
|
||||
|
||||
private val customValue = SendTextField.CustomFee(
|
||||
value = "0.159834",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions = KeyboardActions.Default,
|
||||
symbol = "MATIC",
|
||||
decimals = 18,
|
||||
title = stringReference("Fee up to"),
|
||||
footer = stringReference("Maximum commission amount"),
|
||||
label = stringReference("0.41 \$"),
|
||||
isReadonly = false,
|
||||
)
|
||||
|
||||
private val customValues = persistentListOf(
|
||||
customValue,
|
||||
customValue.copy(
|
||||
value = "400",
|
||||
symbol = "GWEI",
|
||||
title = stringReference("Gas price"),
|
||||
footer = stringReference("Gas Price impacts transaction speed; too low, it may not process"),
|
||||
label = null,
|
||||
),
|
||||
customValue.copy(
|
||||
value = "31400",
|
||||
symbol = "",
|
||||
title = stringReference("Gas limit"),
|
||||
footer = stringReference("Gas Limit is auto-calculated; raise it during network congestion"),
|
||||
label = null,
|
||||
),
|
||||
)
|
||||
|
||||
private val feeSelector = FeeSelectorState.Content(
|
||||
fees = singleFee,
|
||||
selectedFee = FeeType.Market,
|
||||
customValues = persistentListOf(),
|
||||
)
|
||||
|
||||
val feeState = SendStates.FeeState(
|
||||
type = SendUiStateType.Fee,
|
||||
isPrimaryButtonEnabled = false,
|
||||
feeSelectorState = feeSelector,
|
||||
fee = fee,
|
||||
rate = BigDecimal.ONE,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isFeeApproximate = false,
|
||||
notifications = persistentListOf(),
|
||||
isCustomSelected = false,
|
||||
isFeeConvertibleToFiat = true,
|
||||
isTronToken = false,
|
||||
)
|
||||
|
||||
val feeChoosableState = feeState.copy(
|
||||
feeSelectorState = feeSelector.copy(
|
||||
fees = multipleFees,
|
||||
customValues = customValues,
|
||||
),
|
||||
)
|
||||
|
||||
val feeCustomState = feeState.copy(
|
||||
feeSelectorState = feeSelector.copy(
|
||||
fees = multipleFees,
|
||||
customValues = customValues,
|
||||
selectedFee = FeeType.Custom,
|
||||
),
|
||||
isCustomSelected = true,
|
||||
)
|
||||
|
||||
val errorFeeState = feeState.copy(
|
||||
feeSelectorState = FeeSelectorState.Error(null),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal object RecipientStatePreviewData {
|
||||
|
||||
private val defaultRecentItem = SendRecipientListContent(
|
||||
id = "sanctus",
|
||||
title = stringReference("0x391316a070212312312378E88CAc8A0C250"),
|
||||
subtitleEndOffset = 0,
|
||||
subtitleIconRes = R.drawable.ic_arrow_down_24,
|
||||
isVisible = true,
|
||||
isLoading = false,
|
||||
)
|
||||
|
||||
val recipientState = SendStates.RecipientState(
|
||||
addressTextField = SendTextField.RecipientAddress(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
placeholder = stringReference("Enter address"),
|
||||
label = stringReference("Recipient"),
|
||||
isError = false,
|
||||
error = null,
|
||||
isValuePasted = false,
|
||||
),
|
||||
memoTextField = SendTextField.RecipientMemo(
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
keyboardOptions = KeyboardOptions.Default,
|
||||
placeholder = stringReference("Optional"),
|
||||
label = stringReference("Memo"),
|
||||
isError = false,
|
||||
error = null,
|
||||
disabledText = stringReference("Already included in the entered address"),
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
),
|
||||
recent = persistentListOf(),
|
||||
wallets = persistentListOf(),
|
||||
network = "Ethereum",
|
||||
isValidating = false,
|
||||
isPrimaryButtonEnabled = true,
|
||||
)
|
||||
|
||||
val recipientAddressState = recipientState.copy(
|
||||
addressTextField = recipientState.addressTextField.copy(
|
||||
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
|
||||
),
|
||||
)
|
||||
|
||||
val recipientWithRecentState = recipientState.copy(
|
||||
recent = persistentListOf(
|
||||
defaultRecentItem.copy(
|
||||
id = "1",
|
||||
subtitle = stringReference("1 000 000 000.0004 USDT"),
|
||||
timestamp = stringReference("today at 14:46"),
|
||||
subtitleIconRes = R.drawable.ic_arrow_up_24,
|
||||
),
|
||||
defaultRecentItem.copy(
|
||||
id = "2",
|
||||
subtitle = stringReference("20,09 USDT"),
|
||||
timestamp = stringReference("24.05.2004 at 14:46"),
|
||||
),
|
||||
defaultRecentItem.copy(
|
||||
id = "3",
|
||||
subtitle = stringReference("20,09 USDT"),
|
||||
timestamp = stringReference("24.05.2004 at 14:46"),
|
||||
),
|
||||
),
|
||||
wallets = persistentListOf(
|
||||
defaultRecentItem.copy(
|
||||
id = "4",
|
||||
subtitle = stringReference("Main Wallet"),
|
||||
subtitleIconRes = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal object SendClickIntentsStub : SendClickIntents {
|
||||
override fun popBackStack() {}
|
||||
|
||||
override fun onBackClick() {}
|
||||
|
||||
override fun onCloseClick() {}
|
||||
|
||||
override fun onNextClick(isFromEdit: Boolean) {}
|
||||
|
||||
override fun onPrevClick() {}
|
||||
|
||||
override fun onQrCodeScanClick() {}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {}
|
||||
|
||||
override fun onTokenDetailsClick(currency: CryptoCurrency) {}
|
||||
|
||||
override fun onAmountValueChange(value: String) {}
|
||||
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {}
|
||||
|
||||
override fun onAmountNext() {}
|
||||
|
||||
override fun onMaxValueClick() {}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {}
|
||||
|
||||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {}
|
||||
|
||||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {}
|
||||
|
||||
override fun feeReload() {}
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
||||
|
||||
override fun onReadMoreClick() {}
|
||||
|
||||
override fun onSendClick() {}
|
||||
|
||||
override fun showAmount() {}
|
||||
|
||||
override fun showRecipient() {}
|
||||
|
||||
override fun showFee() {}
|
||||
|
||||
override fun showSend() {}
|
||||
|
||||
override fun onExploreClick() {}
|
||||
|
||||
override fun onShareClick(txUrl: String) {}
|
||||
|
||||
override fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>) {}
|
||||
|
||||
override fun onNotificationCancel(clazz: Class<out NotificationUM>) {}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.send.impl.presentation.state.previewdata
|
||||
|
||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
|
||||
internal object SendStatesPreviewData {
|
||||
|
||||
val uiState = SendUiState(
|
||||
clickIntents = SendClickIntentsStub,
|
||||
isEditingDisabled = false,
|
||||
cryptoCurrencyName = "",
|
||||
amountState = AmountStatePreviewData.amountWithValueState,
|
||||
recipientState = RecipientStatePreviewData.recipientAddressState,
|
||||
feeState = FeeStatePreviewData.feeChoosableState,
|
||||
sendState = ConfirmStatePreviewData.sendState,
|
||||
editAmountState = AmountStatePreviewData.amountWithValueState,
|
||||
editRecipientState = RecipientStatePreviewData.recipientAddressState,
|
||||
editFeeState = FeeStatePreviewData.feeChoosableState,
|
||||
isBalanceHidden = false,
|
||||
isSubtracted = false,
|
||||
event = consumedEvent(),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.domain.transaction.error.ValidateAddressError
|
||||
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.StateRouter
|
||||
import com.tangem.utils.Provider
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import timber.log.Timber
|
||||
|
||||
internal class RecipientSendFactory(
|
||||
private val stateRouterProvider: Provider<StateRouter>,
|
||||
private val currentStateProvider: Provider<SendUiState>,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
|
||||
private val validateWalletMemoUseCase: ValidateWalletMemoUseCase,
|
||||
) {
|
||||
private val recipientWalletListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientWalletListConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
isUtxoConsolidationAvailableProvider = isUtxoConsolidationAvailableProvider,
|
||||
)
|
||||
}
|
||||
private val recipientHistoryListStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendRecipientHistoryListConverter(
|
||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
fun onLoadedWalletsList(wallets: List<AvailableWallet?>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
recipientState = state.recipientState?.copy(
|
||||
wallets = recipientWalletListStateConverter.convert(wallets),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onLoadedHistoryList(txHistory: List<TxHistoryItem>): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
return state.copy(
|
||||
recipientState = state.recipientState?.copy(
|
||||
recent = recipientHistoryListStateConverter.convert(txHistory),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onRecipientAddressValueChange(value: String, isXAddress: Boolean = false, isValuePasted: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
addressTextField = recipientState.addressTextField.copy(value = value, isValuePasted = isValuePasted),
|
||||
memoTextField = recipientState.memoTextField?.copy(isEnabled = !isXAddress),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientAddressValidState(
|
||||
value: String,
|
||||
maybeValidAddress: Either<ValidateAddressError, Unit>,
|
||||
): SendUiState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
|
||||
val isValidMemo = validateWalletMemoUseCase(
|
||||
memo = recipientState.memoTextField?.value.orEmpty(),
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e("Failed to validateWalletMemoUseCase: $it")
|
||||
false
|
||||
}
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && maybeValidAddress.isRight(),
|
||||
isValidating = false,
|
||||
addressTextField = recipientState.addressTextField.copy(
|
||||
error = maybeValidAddress.fold(
|
||||
ifLeft = {
|
||||
when (it) {
|
||||
ValidateAddressError.InvalidAddress -> resourceReference(
|
||||
R.string.send_recipient_address_error,
|
||||
)
|
||||
ValidateAddressError.AddressInWallet -> resourceReference(
|
||||
R.string.send_error_address_same_as_wallet,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
ifRight = { null },
|
||||
),
|
||||
isError = value.isNotEmpty() && maybeValidAddress.isLeft(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientAddressValidationStarted(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(isValidating = true),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientMemoValueChange(value: String, isValuePasted: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
value = value,
|
||||
isValuePasted = isValuePasted,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnRecipientMemoValidState(value: String, isValidAddress: Boolean): SendUiState {
|
||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
|
||||
val isValidMemo = validateWalletMemoUseCase(
|
||||
memo = value,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
).getOrElse {
|
||||
Timber.e("Failed to validateWalletMemoUseCase: $it")
|
||||
false
|
||||
}
|
||||
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
isPrimaryButtonEnabled = isValidMemo && isValidAddress,
|
||||
isValidating = false,
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
isError = value.isNotEmpty() && !isValidMemo,
|
||||
isEnabled = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getOnXAddressMemoState(): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
memoTextField = recipientState.memoTextField?.copy(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getHiddenRecentListState(isNotValid: Boolean): SendUiState {
|
||||
val state = currentStateProvider()
|
||||
val isEditState = stateRouterProvider().isEditState
|
||||
val recipientState = state.getRecipientState(isEditState) ?: return state
|
||||
return state.copyWrapped(
|
||||
isEditState = isEditState,
|
||||
recipientState = recipientState.copy(
|
||||
recent = recipientState.recent.map { recent ->
|
||||
recent.copy(isVisible = isNotValid && (recent.isLoading || recent.title != TextReference.EMPTY))
|
||||
}.toPersistentList(),
|
||||
wallets = recipientState.wallets.map { wallet ->
|
||||
wallet.copy(isVisible = isNotValid && (wallet.isLoading || wallet.title != TextReference.EMPTY))
|
||||
}.toPersistentList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
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.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendRecipientAddressFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
) : Converter<String, SendTextField.RecipientAddress> {
|
||||
|
||||
override fun convert(value: String): SendTextField.RecipientAddress {
|
||||
return SendTextField.RecipientAddress(
|
||||
value = value,
|
||||
onValueChange = clickIntents::onRecipientAddressValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Next,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
error = resourceReference(R.string.send_recipient_address_error),
|
||||
placeholder = resourceReference(R.string.send_enter_address_field),
|
||||
label = resourceReference(R.string.send_recipient),
|
||||
isValuePasted = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_DEFAULT_COUNT
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_KEY_TAG
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientHistoryListConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<List<TxHistoryItem>, ImmutableList<SendRecipientListContent>> {
|
||||
|
||||
override fun convert(value: List<TxHistoryItem>): ImmutableList<SendRecipientListContent> {
|
||||
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
|
||||
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 ->
|
||||
SendRecipientListContent(
|
||||
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 -> TextReference.Res(
|
||||
R.string.transaction_history_multiple_addresses,
|
||||
)
|
||||
is TxHistoryItem.DestinationType.Single -> TextReference.Str(destination.addressType.address)
|
||||
}
|
||||
} else {
|
||||
when (val source = sourceType) {
|
||||
is TxHistoryItem.SourceType.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses)
|
||||
is TxHistoryItem.SourceType.Single -> TextReference.Str(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,60 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
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.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendRecipientMemoFieldConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatus: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<SendRecipientMemoFieldConverter.Data, SendTextField.RecipientMemo> {
|
||||
|
||||
fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? {
|
||||
val cryptoCurrency = cryptoCurrencyStatus().currency
|
||||
val memo = memoValue ?: ""
|
||||
|
||||
return when (cryptoCurrency.network.transactionExtrasType) {
|
||||
Network.TransactionExtrasType.NONE -> null
|
||||
Network.TransactionExtrasType.MEMO -> convert(
|
||||
value = Data(
|
||||
memo = memo,
|
||||
label = R.string.send_extras_hint_memo,
|
||||
),
|
||||
)
|
||||
Network.TransactionExtrasType.DESTINATION_TAG -> convert(
|
||||
value = Data(
|
||||
memo = memo,
|
||||
label = R.string.send_destination_tag_field,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun convert(value: Data): SendTextField.RecipientMemo {
|
||||
return SendTextField.RecipientMemo(
|
||||
value = value.memo,
|
||||
onValueChange = clickIntents::onRecipientMemoValueChange,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
placeholder = resourceReference(R.string.send_optional_field),
|
||||
label = resourceReference(value.label),
|
||||
error = resourceReference(R.string.send_memo_destination_tag_error),
|
||||
disabledText = resourceReference(R.string.send_additional_field_already_included),
|
||||
isEnabled = true,
|
||||
isValuePasted = false,
|
||||
)
|
||||
}
|
||||
|
||||
data class Data(val memo: String, @StringRes val label: Int)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.*
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class SendRecipientStateConverter(
|
||||
private val clickIntents: SendClickIntents,
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
) : Converter<SendRecipientStateConverter.Data, SendStates.RecipientState> {
|
||||
|
||||
private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) }
|
||||
private val memoFieldConverter by lazy {
|
||||
SendRecipientMemoFieldConverter(
|
||||
clickIntents,
|
||||
cryptoCurrencyStatusProvider,
|
||||
)
|
||||
}
|
||||
|
||||
override fun convert(value: Data): SendStates.RecipientState {
|
||||
return SendStates.RecipientState(
|
||||
addressTextField = addressFieldConverter.convert(value.address),
|
||||
memoTextField = memoFieldConverter.convertOrNull(value.memo),
|
||||
network = cryptoCurrencyStatusProvider().currency.network.name,
|
||||
isPrimaryButtonEnabled = false,
|
||||
wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT),
|
||||
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
|
||||
)
|
||||
}
|
||||
|
||||
data class Data(val address: String, val memo: String? = null)
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_DEFAULT_COUNT
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.WALLET_KEY_TAG
|
||||
import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendRecipientWalletListConverter(
|
||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||
private val isUtxoConsolidationAvailableProvider: Provider<Boolean>,
|
||||
) :
|
||||
Converter<List<AvailableWallet?>, PersistentList<SendRecipientListContent>> {
|
||||
override fun convert(value: List<AvailableWallet?>): PersistentList<SendRecipientListContent> {
|
||||
return value.filterWallets().ifEmpty {
|
||||
emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
|
||||
var walletsCounter = 0
|
||||
val currentAddress: String = runCatching {
|
||||
cryptoCurrencyStatusProvider().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 || isUtxoConsolidationAvailableProvider())
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
SendRecipientListContent(
|
||||
id = "${WALLET_KEY_TAG}${walletsCounter++}",
|
||||
title = TextReference.Str(wallet.address),
|
||||
subtitle = TextReference.Str(name),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.toPersistentList()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.send.impl.presentation.state.recipient.utils
|
||||
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
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(
|
||||
SendRecipientListContent(
|
||||
id = "$tag$it",
|
||||
isLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
||||
internal fun emptyListState(tag: String, count: Int) = buildList {
|
||||
repeat(count) {
|
||||
add(
|
||||
SendRecipientListContent(
|
||||
id = "$tag$it",
|
||||
isLoading = false,
|
||||
isVisible = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}.toPersistentList()
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import com.tangem.common.ui.alerts.models.AlertUM
|
||||
import com.tangem.core.ui.components.BasicDialog
|
||||
import com.tangem.core.ui.components.DialogButtonUM
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendEvent
|
||||
|
||||
@Composable
|
||||
internal fun SendEventEffect(event: StateEvent<SendEvent>, snackbarHostState: SnackbarHostState) {
|
||||
val resources = LocalContext.current.resources
|
||||
var alertConfig by remember { mutableStateOf<AlertUM?>(value = null) }
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = alertConfig) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
|
||||
alertConfig?.let {
|
||||
SendAlert(state = it, onDismiss = { alertConfig = null })
|
||||
}
|
||||
|
||||
EventEffect(
|
||||
event = event,
|
||||
onTrigger = { value ->
|
||||
when (value) {
|
||||
is SendEvent.ShowSnackBar -> {
|
||||
snackbarHostState.showSnackbar(message = value.text.resolveReference(resources))
|
||||
}
|
||||
is SendEvent.ShowAlert -> {
|
||||
alertConfig = value.alert
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SendAlert(state: AlertUM, onDismiss: () -> Unit) {
|
||||
val confirmButton: DialogButtonUM
|
||||
val dismissButton: DialogButtonUM?
|
||||
|
||||
val onActionClick = state.onConfirmClick
|
||||
if (onActionClick != null) {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = {
|
||||
onActionClick()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
dismissButton = DialogButtonUM(
|
||||
title = stringResourceSafe(id = R.string.common_cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
} else {
|
||||
confirmButton = DialogButtonUM(
|
||||
title = state.confirmButtonText.resolveReference(),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
dismissButton = null
|
||||
}
|
||||
|
||||
BasicDialog(
|
||||
message = state.message.resolveReference(),
|
||||
confirmButton = confirmButton,
|
||||
onDismissDialog = onDismiss,
|
||||
title = state.title?.resolveReference(),
|
||||
dismissButton = dismissButton,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.common.ui.amountScreen.ui.SendDoneButtons
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatString
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(
|
||||
uiState: SendUiState,
|
||||
currentState: SendUiCurrentScreen,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val isSuccess = sendState.isSuccess
|
||||
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess
|
||||
val isSentState = currentState.type == SendUiStateType.Send && isSuccess
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
SendingText(
|
||||
uiState = uiState,
|
||||
isEditState = currentState.isFromConfirmation,
|
||||
isVisible = isSendingState,
|
||||
)
|
||||
SendDoneButtons(
|
||||
txUrl = sendState.txUrl,
|
||||
onExploreClick = uiState.clickIntents::onExploreClick,
|
||||
onShareClick = { uiState.clickIntents.onShareClick(it) },
|
||||
isVisible = isSentState,
|
||||
)
|
||||
SendNavigationButton(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendNavigationButton(
|
||||
uiState: SendUiState,
|
||||
currentState: SendUiCurrentScreen,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val sendState = uiState.sendState ?: return
|
||||
val isEditingDisabled = uiState.isEditingDisabled
|
||||
val isSuccess = sendState.isSuccess
|
||||
val isSending = sendState.isSending
|
||||
|
||||
val isFromConfirmation = currentState.isFromConfirmation
|
||||
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
|
||||
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
|
||||
|
||||
val (buttonTextId, buttonClick) = getButtonData(
|
||||
currentState = currentState,
|
||||
isSuccess = isSuccess,
|
||||
isSending = isSending,
|
||||
uiState = uiState,
|
||||
)
|
||||
val isButtonEnabled = isButtonEnabled(currentState, uiState)
|
||||
val buttonIcon = if (isSendingState) {
|
||||
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
|
||||
} else {
|
||||
TangemButtonIconPosition.None
|
||||
}
|
||||
|
||||
Row(modifier = modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation,
|
||||
enter = expandHorizontally(expandFrom = Alignment.End),
|
||||
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
|
||||
) {
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back_24),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable { uiState.clickIntents.onPrevClick() }
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
SpacerW12()
|
||||
}
|
||||
}
|
||||
TangemButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResourceSafe(buttonTextId),
|
||||
icon = buttonIcon,
|
||||
enabled = isButtonEnabled,
|
||||
onClick = {
|
||||
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
buttonClick()
|
||||
},
|
||||
showProgress = false,
|
||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendingText(
|
||||
uiState: SendUiState,
|
||||
isEditState: Boolean,
|
||||
isVisible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var isVisibleProxy by remember { mutableStateOf(isVisible) }
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
// the text should appear when the keyboard is closed
|
||||
LaunchedEffect(isVisible, keyboard) {
|
||||
if (isVisible && keyboard is Keyboard.Opened) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
isVisibleProxy = isVisible
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisibleProxy,
|
||||
modifier = modifier,
|
||||
enter = slideInVertically() + fadeIn(),
|
||||
exit = fadeOut(tween(durationMillis = 300)),
|
||||
label = "Animate show sending state text",
|
||||
) {
|
||||
val amountState = uiState.getAmountState(isEditState) as? AmountState.Data
|
||||
val feeState = uiState.getFeeState(isEditState)
|
||||
val fiatRate = feeState?.rate
|
||||
val fiatAmount = amountState?.amountTextField?.fiatAmount
|
||||
val feeFiat = fiatRate?.let { feeState.fee?.amount?.value?.multiply(it) }
|
||||
val sendingFiat = if (uiState.isSubtracted) {
|
||||
fiatAmount?.value
|
||||
} else {
|
||||
if (feeState?.isFeeConvertibleToFiat == true) {
|
||||
feeFiat?.let { fiatAmount?.value?.plus(it) }
|
||||
} else {
|
||||
fiatAmount?.value
|
||||
}
|
||||
}
|
||||
|
||||
if (feeFiat != null && sendingFiat != null) {
|
||||
val sendingValue = BigDecimalFormatter.formatFiatAmount(
|
||||
fiatAmount = sendingFiat,
|
||||
fiatCurrencySymbol = feeState.appCurrency.symbol,
|
||||
fiatCurrencyCode = feeState.appCurrency.code,
|
||||
)
|
||||
val textResource = remember(uiState) {
|
||||
val fee = feeState.fee
|
||||
if (feeState.isTronToken && fee is Fee.Tron) {
|
||||
getTokenFeeSendingText(
|
||||
feeState = feeState,
|
||||
fee = fee,
|
||||
sendingValue = sendingValue,
|
||||
)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = if (feeState.isFeeConvertibleToFiat) {
|
||||
R.string.send_summary_transaction_description
|
||||
} else {
|
||||
R.string.send_summary_transaction_description_no_fiat_fee
|
||||
},
|
||||
formatArgs = wrappedList(sendingValue, feeState.getFiatValue()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = textResource.resolveAnnotatedReference(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) {
|
||||
getFiatString(
|
||||
value = fee?.amount?.value,
|
||||
rate = rate,
|
||||
appCurrency = appCurrency,
|
||||
)
|
||||
} else {
|
||||
val amount = fee?.amount
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
decimals = amount?.decimals ?: 0,
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
).fee(
|
||||
canBeLower = isFeeApproximate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonData(
|
||||
uiState: SendUiState,
|
||||
currentState: SendUiCurrentScreen,
|
||||
isSuccess: Boolean,
|
||||
isSending: Boolean,
|
||||
): Pair<Int, () -> Unit> {
|
||||
return when (currentState.type) {
|
||||
SendUiStateType.None,
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.Fee,
|
||||
-> R.string.common_next to { uiState.clickIntents.onNextClick() }
|
||||
SendUiStateType.EditFee,
|
||||
SendUiStateType.EditAmount,
|
||||
SendUiStateType.EditRecipient,
|
||||
-> R.string.common_continue to { uiState.clickIntents.onNextClick(isFromEdit = true) }
|
||||
SendUiStateType.Send -> when {
|
||||
isSuccess -> R.string.common_close
|
||||
isSending -> R.string.send_sending
|
||||
else -> R.string.common_send
|
||||
} to uiState.clickIntents::onSendClick
|
||||
}
|
||||
}
|
||||
|
||||
private fun isButtonEnabled(currentState: SendUiCurrentScreen, uiState: SendUiState): Boolean {
|
||||
return when (currentState.type) {
|
||||
SendUiStateType.Amount -> uiState.amountState.isPrimaryButtonEnabled
|
||||
SendUiStateType.Recipient -> uiState.recipientState?.isPrimaryButtonEnabled
|
||||
SendUiStateType.Fee -> uiState.feeState?.isPrimaryButtonEnabled
|
||||
SendUiStateType.Send -> uiState.sendState?.isPrimaryButtonEnabled
|
||||
SendUiStateType.EditAmount -> uiState.editAmountState.isPrimaryButtonEnabled
|
||||
SendUiStateType.EditRecipient -> uiState.editRecipientState?.isPrimaryButtonEnabled
|
||||
SendUiStateType.EditFee -> uiState.editFeeState?.isPrimaryButtonEnabled
|
||||
else -> true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun getTokenFeeSendingText(feeState: SendStates.FeeState, fee: Fee.Tron, sendingValue: String): TextReference {
|
||||
val suffix = when {
|
||||
fee.remainingEnergy == 0L -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_including,
|
||||
wrappedList(feeState.getFiatValue()),
|
||||
)
|
||||
}
|
||||
fee.feeEnergy <= fee.remainingEnergy -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_fee_covered,
|
||||
wrappedList(fee.feeEnergy),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_fee_reduced,
|
||||
wrappedList(fee.remainingEnergy),
|
||||
)
|
||||
}
|
||||
}
|
||||
val prefix = resourceReference(
|
||||
R.string.send_summary_transaction_description_prefix,
|
||||
wrappedList(sendingValue),
|
||||
)
|
||||
|
||||
return combinedReference(prefix, COMMA_SEPARATOR, suffix)
|
||||
}
|
||||
|
||||
private val COMMA_SEPARATOR = stringReference(", ")
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
package com.tangem.features.send.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenContent
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiStateType
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData
|
||||
import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent
|
||||
import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent
|
||||
import com.tangem.features.send.impl.presentation.ui.send.SendContent
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.withIndex
|
||||
|
||||
@Composable
|
||||
internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val onBackClick = uiState.clickIntents::onBackClick.takeIf {
|
||||
uiState.sendState?.isSending != true
|
||||
} ?: {}
|
||||
BackHandler(onBack = onBackClick)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SendAppBar(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
)
|
||||
SendScreenContent(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SendNavigationButtons(
|
||||
uiState = uiState,
|
||||
currentState = currentState,
|
||||
)
|
||||
}
|
||||
|
||||
SendEventEffect(
|
||||
event = uiState.event,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) {
|
||||
val (titleRes, subtitleRes) = when (currentState.type) {
|
||||
SendUiStateType.Amount,
|
||||
SendUiStateType.EditAmount,
|
||||
-> resourceReference(R.string.send_amount_label) to null
|
||||
SendUiStateType.Recipient,
|
||||
SendUiStateType.EditRecipient,
|
||||
-> resourceReference(R.string.send_recipient_label) to null
|
||||
SendUiStateType.Fee,
|
||||
SendUiStateType.EditFee,
|
||||
-> resourceReference(R.string.common_fee_selector_title) to null
|
||||
SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) {
|
||||
resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to
|
||||
(uiState.amountState as? AmountState.Data)?.title
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
else -> null to null
|
||||
}
|
||||
val iconRes = if (currentState.type == SendUiStateType.Recipient) {
|
||||
R.drawable.ic_qrcode_scan_24
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val backIcon = when (currentState.type) {
|
||||
SendUiStateType.EditAmount,
|
||||
SendUiStateType.EditFee,
|
||||
SendUiStateType.EditRecipient,
|
||||
-> R.drawable.ic_back_24
|
||||
else -> R.drawable.ic_close_24
|
||||
}
|
||||
AppBarWithBackButtonAndIcon(
|
||||
text = titleRes?.resolveReference(),
|
||||
subtitle = subtitleRes?.resolveReference(),
|
||||
onBackClick = uiState.clickIntents::onCloseClick,
|
||||
onIconClick = uiState.clickIntents::onQrCodeScanClick,
|
||||
backIconRes = backIcon,
|
||||
iconRes = iconRes,
|
||||
backgroundColor = TangemTheme.colors.background.tertiary,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier) {
|
||||
var currentStateProxy by remember { mutableStateOf(currentState) }
|
||||
var isTransitionAnimationRunning by remember { mutableStateOf(false) }
|
||||
|
||||
// Prevent quick screen changes to avoid some of the transition animation distortions
|
||||
LaunchedEffect(currentState) {
|
||||
snapshotFlow { isTransitionAnimationRunning }
|
||||
.withIndex()
|
||||
.map { (index, running) ->
|
||||
if (running && index != 0) {
|
||||
delay(timeMillis = 200)
|
||||
}
|
||||
running
|
||||
}
|
||||
.first { !it }
|
||||
|
||||
currentStateProxy = currentState
|
||||
}
|
||||
// Restrict pressing the back button while screen transition is running to avoid most of the animation distortions
|
||||
BackHandler(enabled = isTransitionAnimationRunning) {}
|
||||
|
||||
// Box is needed to fix animation with resizing of AnimatedContent
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
AnimatedContent(
|
||||
targetState = currentStateProxy,
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
label = "Send Scree Navigation",
|
||||
transitionSpec = {
|
||||
val direction = if (initialState.type.ordinal < targetState.type.ordinal) {
|
||||
AnimatedContentTransitionScope.SlideDirection.Start
|
||||
} else {
|
||||
AnimatedContentTransitionScope.SlideDirection.End
|
||||
}
|
||||
|
||||
slideIntoContainer(towards = direction, animationSpec = tween())
|
||||
.togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween()))
|
||||
},
|
||||
) { state ->
|
||||
isTransitionAnimationRunning = transition.targetState != transition.currentState
|
||||
|
||||
when (state.type) {
|
||||
SendUiStateType.Amount -> AmountScreenContent(
|
||||
amountState = uiState.amountState,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
SendUiStateType.EditAmount -> AmountScreenContent(
|
||||
amountState = uiState.editAmountState,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
clickIntents = uiState.clickIntents,
|
||||
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
SendUiStateType.Recipient -> SendRecipientContent(
|
||||
uiState = uiState.recipientState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
)
|
||||
SendUiStateType.EditRecipient -> SendRecipientContent(
|
||||
uiState = uiState.editRecipientState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
isBalanceHidden = uiState.isBalanceHidden,
|
||||
)
|
||||
SendUiStateType.EditFee -> SendSpeedAndFeeContent(
|
||||
state = uiState.editFeeState,
|
||||
clickIntents = uiState.clickIntents,
|
||||
)
|
||||
SendUiStateType.Send -> SendContent(uiState)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 736)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 736, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendScreen_Preview(@PreviewParameter(SendScreenPreviewProvider::class) data: SendScreenPreview) {
|
||||
TangemThemePreview {
|
||||
SendScreen(
|
||||
uiState = data.uiState,
|
||||
currentState = data.currentState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendScreenPreviewProvider : PreviewParameterProvider<SendScreenPreview> {
|
||||
override val values: Sequence<SendScreenPreview>
|
||||
get() = sequenceOf(
|
||||
SendScreenPreview(
|
||||
uiState = SendStatesPreviewData.uiState,
|
||||
currentState = SendUiCurrentScreen(type = SendUiStateType.Recipient, isFromConfirmation = false),
|
||||
),
|
||||
SendScreenPreview(
|
||||
uiState = SendStatesPreviewData.uiState,
|
||||
currentState = SendUiCurrentScreen(type = SendUiStateType.Amount, isFromConfirmation = false),
|
||||
),
|
||||
SendScreenPreview(
|
||||
uiState = SendStatesPreviewData.uiState,
|
||||
currentState = SendUiCurrentScreen(type = SendUiStateType.Send, isFromConfirmation = false),
|
||||
),
|
||||
SendScreenPreview(
|
||||
uiState = SendStatesPreviewData.uiState,
|
||||
currentState = SendUiCurrentScreen(type = SendUiStateType.EditFee, isFromConfirmation = true),
|
||||
),
|
||||
SendScreenPreview(
|
||||
uiState = SendStatesPreviewData.uiState.copy(sendState = ConfirmStatePreviewData.sendDoneState),
|
||||
currentState = SendUiCurrentScreen(type = SendUiStateType.Send, isFromConfirmation = false),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private data class SendScreenPreview(
|
||||
val uiState: SendUiState,
|
||||
val currentState: SendUiCurrentScreen,
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal fun LazyListScope.notifications(
|
||||
notifications: ImmutableList<NotificationUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
hasPaddingAbove: Boolean = false,
|
||||
isClickDisabled: Boolean = false,
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = notifications,
|
||||
key = { _, item -> item::class.java },
|
||||
contentType = { _, item -> item::class.java },
|
||||
itemContent = { i, item ->
|
||||
val topPadding = if (i == 0 && hasPaddingAbove) {
|
||||
TangemTheme.dimens.spacing0
|
||||
} else {
|
||||
TangemTheme.dimens.spacing12
|
||||
}
|
||||
Notification(
|
||||
config = item.config,
|
||||
modifier = modifier
|
||||
.padding(top = topPadding)
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
containerColor = when (item) {
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning.NetworkFeeUnreachable,
|
||||
is NotificationUM.Warning.HighFeeError,
|
||||
-> TangemTheme.colors.background.action
|
||||
else -> TangemTheme.colors.button.disabled
|
||||
},
|
||||
iconTint = when (item) {
|
||||
is NotificationUM.Error.TokenExceedsBalance,
|
||||
is NotificationUM.Warning,
|
||||
-> null
|
||||
is NotificationUM.Error -> TangemTheme.colors.icon.warning
|
||||
is NotificationUM.Info -> TangemTheme.colors.icon.accent
|
||||
},
|
||||
isEnabled = !isClickDisabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
|
||||
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun SendCustomFee(
|
||||
customValues: ImmutableList<SendTextField.CustomFee>,
|
||||
selectedFee: FeeType,
|
||||
hasNotifications: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = selectedFee == FeeType.Custom && customValues.isNotEmpty(),
|
||||
label = "Custom Fee Selected Animation",
|
||||
enter = expandVertically().plus(fadeIn()),
|
||||
exit = shrinkVertically().plus(fadeOut()),
|
||||
) {
|
||||
val bottomPadding = if (hasNotifications) {
|
||||
TangemTheme.dimens.spacing12
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = modifier.padding(bottom = bottomPadding),
|
||||
) {
|
||||
repeat(customValues.size) { index ->
|
||||
val value = customValues[index]
|
||||
FooterContainer(
|
||||
footer = value.footer,
|
||||
) {
|
||||
if (value.label != null) {
|
||||
InputRowEnterInfoAmount(
|
||||
text = value.value,
|
||||
decimals = value.decimals,
|
||||
symbol = value.symbol,
|
||||
title = value.title,
|
||||
info = value.label,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
keyboardActions = value.keyboardActions,
|
||||
onValueChange = value.onValueChange,
|
||||
showDivider = false,
|
||||
isReadOnly = value.isReadonly,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
InputRowEnterAmount(
|
||||
text = value.value,
|
||||
decimals = value.decimals,
|
||||
title = value.title,
|
||||
symbol = value.symbol,
|
||||
onValueChange = value.onValueChange,
|
||||
keyboardOptions = value.keyboardOptions,
|
||||
keyboardActions = value.keyboardActions,
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.action,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
|
||||
import com.tangem.features.send.impl.presentation.ui.common.notifications
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY"
|
||||
private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY"
|
||||
|
||||
@Composable
|
||||
internal fun SendSpeedAndFeeContent(state: SendStates.FeeState?, clickIntents: SendClickIntents) {
|
||||
if (state == null) return
|
||||
val feeSendState = state.feeSelectorState as? FeeSelectorState.Content
|
||||
val notifications = state.notifications
|
||||
val isCustomSelected = feeSendState?.selectedFee == FeeType.Custom
|
||||
val hasNotifications = notifications.isNotEmpty()
|
||||
LazyColumn(
|
||||
modifier = Modifier // Do not put fillMaxSize() in here
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
feeSelector(state, clickIntents)
|
||||
if (feeSendState != null) {
|
||||
customFee(feeSendState = feeSendState, hasNotifications = hasNotifications)
|
||||
}
|
||||
notifications(notifications = notifications, hasPaddingAbove = isCustomSelected)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.feeSelector(state: SendStates.FeeState, clickIntents: SendClickIntents) {
|
||||
item(
|
||||
key = FEE_SELECTOR_KEY,
|
||||
) {
|
||||
SendSpeedSelector(
|
||||
state = state,
|
||||
clickIntents = clickIntents,
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
internal fun LazyListScope.customFee(
|
||||
feeSendState: FeeSelectorState.Content,
|
||||
hasNotifications: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
item(
|
||||
key = FEE_CUSTOM_KEY,
|
||||
) {
|
||||
SendCustomFee(
|
||||
customValues = feeSendState.customValues,
|
||||
selectedFee = feeSendState.selectedFee,
|
||||
hasNotifications = hasNotifications,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateItemPlacement()
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(top = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendSpeedAndFeeContent_Preview(
|
||||
@PreviewParameter(FeeStatePreviewProvider::class) feeState: SendStates.FeeState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
SendSpeedAndFeeContent(
|
||||
state = feeState,
|
||||
clickIntents = SendClickIntentsStub,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeStatePreviewProvider : PreviewParameterProvider<SendStates.FeeState> {
|
||||
override val values: Sequence<SendStates.FeeState>
|
||||
get() = sequenceOf(
|
||||
FeeStatePreviewData.feeState,
|
||||
FeeStatePreviewData.feeChoosableState,
|
||||
FeeStatePreviewData.feeCustomState,
|
||||
FeeStatePreviewData.errorFeeState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendSpeedSelector(
|
||||
state: SendStates.FeeState,
|
||||
clickIntents: SendClickIntents,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
feeType = FeeType.Slow,
|
||||
state = state,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_market,
|
||||
iconRes = R.drawable.ic_bird_24,
|
||||
feeType = FeeType.Market,
|
||||
state = state,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_fee_selector_option_fast,
|
||||
iconRes = R.drawable.ic_hare_24,
|
||||
feeType = FeeType.Fast,
|
||||
state = state,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
|
||||
)
|
||||
SendSpeedSelectorItem(
|
||||
titleRes = R.string.common_custom,
|
||||
iconRes = R.drawable.ic_edit_24,
|
||||
feeType = FeeType.Custom,
|
||||
state = state,
|
||||
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) },
|
||||
)
|
||||
}
|
||||
FooterText(clickIntents::onReadMoreClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FooterText(onReadMoreClick: () -> Unit) {
|
||||
val linkText = stringResourceSafe(R.string.common_read_more)
|
||||
val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText)
|
||||
val linkTextPosition = fullString.length - linkText.length
|
||||
val defaultStyle = TangemTheme.colors.text.tertiary
|
||||
val linkStyle = TangemTheme.colors.text.accent
|
||||
val annotatedString = remember(defaultStyle, linkStyle) {
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(defaultStyle)) {
|
||||
append(fullString.substring(0, linkTextPosition))
|
||||
}
|
||||
withStyle(SpanStyle(linkStyle)) {
|
||||
append(fullString.substring(linkTextPosition, fullString.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val click = { i: Int ->
|
||||
val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1))
|
||||
if (i in readMoreStyle.start..readMoreStyle.end) {
|
||||
onReadMoreClick()
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start),
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
onClick = click,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendSpeedSelectorPreview(
|
||||
@PreviewParameter(SendSpeedSelectorPreviewProvider::class) feeState: SendStates.FeeState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
SendSpeedSelector(state = feeState, clickIntents = SendClickIntentsStub)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendSpeedSelectorPreviewProvider : PreviewParameterProvider<SendStates.FeeState> {
|
||||
override val values: Sequence<SendStates.FeeState>
|
||||
get() = sequenceOf(
|
||||
FeeStatePreviewData.feeState,
|
||||
FeeStatePreviewData.errorFeeState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.fee
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
|
||||
@Composable
|
||||
internal fun SendSpeedSelectorItem(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
feeType: FeeType,
|
||||
state: SendStates.FeeState,
|
||||
onSelect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val feeSelectorState = state.feeSelectorState
|
||||
val content = feeSelectorState as? FeeSelectorState.Content
|
||||
val amount = content?.getAmount(feeType)
|
||||
val (showDivider, isVisible) = content.getDividerAndVisibility(feeType)
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
label = "Fee Selector Visibility Animation",
|
||||
enter = expandVertically().plus(fadeIn()),
|
||||
exit = shrinkVertically().plus(fadeOut()),
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect() },
|
||||
) {
|
||||
SelectorRowItem(
|
||||
titleRes = titleRes,
|
||||
iconRes = iconRes,
|
||||
onSelect = onSelect,
|
||||
modifier = modifier,
|
||||
preDot = stringReference(
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
decimals = amount?.decimals ?: 0,
|
||||
).fee(canBeLower = state.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (state.isFeeConvertibleToFiat) {
|
||||
getFiatReference(amount?.value, state.rate, state.appCurrency)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = amount?.currencySymbol?.length,
|
||||
isSelected = content?.selectedFee == feeType,
|
||||
showDivider = showDivider,
|
||||
)
|
||||
FeeLoading(feeSelectorState)
|
||||
FeeError(feeSelectorState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeLoading(feeSelectorState: FeeSelectorState) {
|
||||
Row {
|
||||
SpacerWMax()
|
||||
AnimatedVisibility(
|
||||
visible = feeSelectorState == FeeSelectorState.Loading,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing18,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
)
|
||||
.size(
|
||||
height = TangemTheme.dimens.size12,
|
||||
width = TangemTheme.dimens.size90,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeError(feeSelectorState: FeeSelectorState) {
|
||||
Row {
|
||||
SpacerWMax()
|
||||
AnimatedVisibility(
|
||||
visible = feeSelectorState is FeeSelectorState.Error,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens.spacing14,
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.Content.getAmount(feeType: FeeType): Amount? {
|
||||
val choosableFees = fees as? TransactionFee.Choosable
|
||||
val decimals = fees.normal.amount.decimals
|
||||
val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals)
|
||||
val customAmount = fees.normal.amount.copy(value = customValue)
|
||||
return when (feeType) {
|
||||
FeeType.Slow -> choosableFees?.minimum?.amount
|
||||
FeeType.Market -> fees.normal.amount
|
||||
FeeType.Fast -> choosableFees?.priority?.amount
|
||||
FeeType.Custom -> customAmount
|
||||
}
|
||||
}
|
||||
|
||||
private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): Pair<Boolean, Boolean> {
|
||||
val hasCustomValues = !this?.customValues.isNullOrEmpty()
|
||||
val isNotSingle = this?.fees !is TransactionFee.Single
|
||||
return when (feeType) {
|
||||
FeeType.Slow -> true to isNotSingle
|
||||
FeeType.Market -> (isNotSingle || hasCustomValues) to true
|
||||
FeeType.Fast -> hasCustomValues to isNotSingle
|
||||
FeeType.Custom -> false to hasCustomValues
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
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 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.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.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 = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
IdentIcon(
|
||||
address = title,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius20)),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size36)
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
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 = TangemTheme.dimens.spacing2)
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.background(TangemTheme.colors.background.tertiary, CircleShape)
|
||||
.padding(TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
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 = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
CircleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size40),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size36)
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.spacing70,
|
||||
height = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
width = TangemTheme.dimens.spacing52,
|
||||
height = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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,279 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
import android.content.res.Configuration
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
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.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
|
||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||
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 SendRecipientContent(
|
||||
uiState: SendStates.RecipientState?,
|
||||
clickIntents: SendClickIntents,
|
||||
isBalanceHidden: Boolean,
|
||||
) {
|
||||
if (uiState == null) return
|
||||
val recipients = uiState.recent
|
||||
val wallets = uiState.wallets
|
||||
val memoField = uiState.memoTextField
|
||||
val address = uiState.addressTextField
|
||||
val isValidating by remember(uiState.isValidating) { derivedStateOf { uiState.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 = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
bottom = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
addressItem(
|
||||
address = address,
|
||||
network = uiState.network,
|
||||
isError = isError,
|
||||
isValidating = isValidating,
|
||||
onAddressChange = clickIntents::onRecipientAddressValueChange,
|
||||
)
|
||||
memoField(
|
||||
memoField = memoField,
|
||||
onMemoChange = { clickIntents.onRecipientMemoValueChange(it, true) },
|
||||
)
|
||||
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: SendTextField.RecipientAddress,
|
||||
network: String,
|
||||
isError: Boolean,
|
||||
isValidating: Boolean,
|
||||
onAddressChange: (String, EnterAddressSource?) -> Unit,
|
||||
) {
|
||||
item(key = ADDRESS_FIELD_KEY) {
|
||||
FooterContainer(
|
||||
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(network)),
|
||||
) {
|
||||
InputRowRecipient(
|
||||
value = address.value,
|
||||
title = address.label,
|
||||
placeholder = address.placeholder,
|
||||
onValueChange = address.onValueChange,
|
||||
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: SendTextField.RecipientMemo?, onMemoChange: (String) -> 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 = memoField.onValueChange,
|
||||
onPasteClick = onMemoChange,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),
|
||||
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) {
|
||||
TangemTheme.dimens.spacing20 to TangemTheme.dimens.spacing12
|
||||
} else {
|
||||
TangemTheme.dimens.spacing0 to TangemTheme.dimens.spacing8
|
||||
}
|
||||
val topRadius = if (isFirst) {
|
||||
TangemTheme.dimens.radius16
|
||||
} else {
|
||||
TangemTheme.dimens.radius0
|
||||
}
|
||||
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 = TangemTheme.dimens.spacing12,
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.listItem(
|
||||
list: ImmutableList<SendRecipientListContent>,
|
||||
clickIntents: SendClickIntents,
|
||||
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()
|
||||
AnimateRecentAppearance(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
|
||||
.then(
|
||||
if (isLast && index == list.lastIndex) {
|
||||
Modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = TangemTheme.dimens.radius16,
|
||||
bottomEnd = TangemTheme.dimens.radius16,
|
||||
),
|
||||
)
|
||||
} 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendRecipientContent_Preview(
|
||||
@PreviewParameter(SendRecipientContentPreviewProvider::class) recipientState: SendStates.RecipientState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
SendRecipientContent(
|
||||
uiState = recipientState,
|
||||
clickIntents = SendClickIntentsStub,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendRecipientContentPreviewProvider : PreviewParameterProvider<SendStates.RecipientState> {
|
||||
override val values: Sequence<SendStates.RecipientState>
|
||||
get() = sequenceOf(
|
||||
RecipientStatePreviewData.recipientWithRecentState,
|
||||
RecipientStatePreviewData.recipientState,
|
||||
RecipientStatePreviewData.recipientAddressState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.recipient
|
||||
|
||||
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 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 = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Row {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
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 = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
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,149 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.amountScreen.utils.getFiatReference
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.fee
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.clickable(enabled = !isClickDisabled, onClick = onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_network_fee_title),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
val feeSelectorState = feeState.feeSelectorState
|
||||
val feeAmount = feeState.fee?.amount
|
||||
val (title, icon) = if (feeSelectorState is FeeSelectorState.Content) {
|
||||
when (feeSelectorState.selectedFee) {
|
||||
FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24
|
||||
FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
|
||||
FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24
|
||||
FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24
|
||||
}
|
||||
} else {
|
||||
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
|
||||
}
|
||||
SelectorRowItem(
|
||||
titleRes = title,
|
||||
iconRes = icon,
|
||||
preDot = stringReference(
|
||||
feeAmount?.value.format {
|
||||
crypto(
|
||||
symbol = feeAmount?.currencySymbol.orEmpty(),
|
||||
decimals = feeAmount?.decimals ?: 0,
|
||||
).fee(canBeLower = feeState.isFeeApproximate)
|
||||
},
|
||||
),
|
||||
postDot = if (feeState.isFeeConvertibleToFiat) {
|
||||
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
ellipsizeOffset = feeAmount?.currencySymbol?.length,
|
||||
isSelected = true,
|
||||
showDivider = false,
|
||||
showSelectedAppearance = false,
|
||||
paddingValues = PaddingValues(),
|
||||
)
|
||||
FeeLoading(feeSelectorState)
|
||||
FeeError(feeSelectorState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeLoading(feeSelectorState: FeeSelectorState) {
|
||||
AnimatedContent(
|
||||
targetState = feeSelectorState,
|
||||
label = "Fee Loading State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it == FeeSelectorState.Loading) {
|
||||
RectangleShimmer(
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
modifier = Modifier.size(
|
||||
height = TangemTheme.dimens.size12,
|
||||
width = TangemTheme.dimens.size90,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) {
|
||||
AnimatedContent(
|
||||
targetState = feeSelectorState,
|
||||
label = "Fee Error State Change",
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
if (it is FeeSelectorState.Error) {
|
||||
Text(
|
||||
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: SendStates.FeeState) {
|
||||
TangemThemePreview {
|
||||
FeeBlock(
|
||||
feeState = value,
|
||||
isClickDisabled = true,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class FeeBlockPreviewProvider : PreviewParameterProvider<SendStates.FeeState> {
|
||||
|
||||
override val values: Sequence<SendStates.FeeState>
|
||||
get() = sequenceOf(
|
||||
FeeStatePreviewData.feeState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData
|
||||
|
||||
@Composable
|
||||
internal fun RecipientBlock(
|
||||
recipientState: SendStates.RecipientState,
|
||||
isClickDisabled: Boolean,
|
||||
isEditingDisabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val backgroundColor = if (isEditingDisabled) {
|
||||
TangemTheme.colors.button.disabled
|
||||
} else {
|
||||
TangemTheme.colors.background.action
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(backgroundColor)
|
||||
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AddressBlock(recipientState.addressTextField)
|
||||
MemoBlock(recipientState.memoTextField)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddressBlock(address: SendTextField.RecipientAddress) {
|
||||
Text(
|
||||
text = address.label.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
IdentIcon(
|
||||
address = address.value,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
)
|
||||
Text(
|
||||
text = address.value,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MemoBlock(memo: SendTextField.RecipientMemo?) {
|
||||
val showMemo = memo != null && memo.value.isNotBlank()
|
||||
if (showMemo) {
|
||||
HorizontalDivider(
|
||||
color = TangemTheme.colors.icon.inactive,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
)
|
||||
Text(
|
||||
text = memo?.label?.resolveReference().orEmpty(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = memo?.value.orEmpty(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun RecipientBlockPreview(
|
||||
@PreviewParameter(RecipientBlockPreviewProvider::class) value: SendStates.RecipientState,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
RecipientBlock(
|
||||
recipientState = value,
|
||||
isClickDisabled = true,
|
||||
isEditingDisabled = false,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipientBlockPreviewProvider : PreviewParameterProvider<SendStates.RecipientState> {
|
||||
|
||||
override val values: Sequence<SendStates.RecipientState>
|
||||
get() = sequenceOf(
|
||||
RecipientStatePreviewData.recipientState,
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package com.tangem.features.send.impl.presentation.ui.send
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.amountScreen.ui.AmountBlock
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.send.impl.R
|
||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData
|
||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData
|
||||
import com.tangem.features.send.impl.presentation.ui.common.notifications
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val TAP_HELP_KEY = "TAP_HELP_KEY"
|
||||
private const val BLOCKS_KEY = "BLOCKS_KEY"
|
||||
private const val TAP_HELP_ANIMATION_DELAY = 500L
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun SendContent(uiState: SendUiState) {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val isClickDisabled = sendState.isSending || sendState.isSuccess
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
blocks(uiState)
|
||||
tapHelp(isDisplay = sendState.showTapHelp)
|
||||
notifications(notifications = sendState.notifications, isClickDisabled = isClickDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.blocks(uiState: SendUiState) {
|
||||
val amountState = uiState.amountState
|
||||
val recipientState = uiState.recipientState ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val sendState = uiState.sendState ?: return
|
||||
val isSuccess = sendState.isSuccess
|
||||
val isClickDisabled = sendState.isSending || isSuccess
|
||||
val timestamp = sendState.transactionDate
|
||||
|
||||
item(key = BLOCKS_KEY) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) {
|
||||
AnimatedVisibility(
|
||||
visible = isSuccess,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.sent_transaction_sent_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_date_format,
|
||||
wrappedList(
|
||||
timestamp.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
timestamp.toTimeFormat(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
RecipientBlock(
|
||||
recipientState = recipientState,
|
||||
isClickDisabled = isClickDisabled,
|
||||
isEditingDisabled = uiState.isEditingDisabled,
|
||||
onClick = uiState.clickIntents::showRecipient,
|
||||
)
|
||||
AmountBlock(
|
||||
amountState = amountState,
|
||||
isClickDisabled = isClickDisabled,
|
||||
isEditingDisabled = uiState.isEditingDisabled,
|
||||
onClick = uiState.clickIntents::showAmount,
|
||||
)
|
||||
FeeBlock(
|
||||
feeState = feeState,
|
||||
isClickDisabled = isClickDisabled,
|
||||
onClick = uiState.clickIntents::showFee,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) {
|
||||
item(key = TAP_HELP_KEY) {
|
||||
val animationState = remember { MutableTransitionState(false) }
|
||||
|
||||
LaunchedEffect(key1 = isDisplay) {
|
||||
delay(TAP_HELP_ANIMATION_DELAY)
|
||||
animationState.targetState = isDisplay
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visibleState = animationState,
|
||||
label = "Tap Help Animation",
|
||||
enter = slideInVertically(
|
||||
initialOffsetY = { it / 2 },
|
||||
).plus(fadeIn()),
|
||||
exit = slideOutVertically().plus(fadeOut()),
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.padding(top = TangemTheme.dimens.spacing20),
|
||||
) {
|
||||
val background = TangemTheme.colors.button.secondary
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.send_hint_shape_12),
|
||||
tint = TangemTheme.colors.button.secondary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.send_summary_tap_hint),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(background)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun SendContent_Preview(@PreviewParameter(SendContentPreviewProvider::class) uiState: SendUiState) {
|
||||
TangemThemePreview {
|
||||
SendContent(
|
||||
uiState = uiState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class SendContentPreviewProvider : PreviewParameterProvider<SendUiState> {
|
||||
override val values: Sequence<SendUiState>
|
||||
get() = sequenceOf(
|
||||
SendStatesPreviewData.uiState,
|
||||
SendStatesPreviewData.uiState.copy(sendState = ConfirmStatePreviewData.sendDoneState),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.send.impl.presentation.viewmodel
|
||||
|
||||
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource
|
||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal interface SendClickIntents : AmountScreenClickIntents {
|
||||
|
||||
fun popBackStack()
|
||||
|
||||
fun onBackClick()
|
||||
|
||||
fun onCloseClick()
|
||||
|
||||
fun onNextClick(isFromEdit: Boolean = false)
|
||||
|
||||
fun onPrevClick()
|
||||
|
||||
fun onQrCodeScanClick()
|
||||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
|
||||
fun onTokenDetailsClick(currency: CryptoCurrency)
|
||||
|
||||
// region Recipient
|
||||
fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null)
|
||||
|
||||
fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false)
|
||||
// endregion
|
||||
|
||||
// region Fee
|
||||
fun feeReload()
|
||||
|
||||
fun onFeeSelectorClick(feeType: FeeType)
|
||||
|
||||
fun onCustomFeeValueChange(index: Int, value: String)
|
||||
|
||||
fun onReadMoreClick()
|
||||
// endregion
|
||||
|
||||
// region Send
|
||||
fun onSendClick()
|
||||
|
||||
fun showAmount()
|
||||
|
||||
fun showRecipient()
|
||||
|
||||
fun showFee()
|
||||
|
||||
fun showSend()
|
||||
|
||||
fun onExploreClick()
|
||||
|
||||
fun onShareClick(txUrl: String)
|
||||
|
||||
fun onAmountReduceByClick(
|
||||
reduceAmountBy: BigDecimal,
|
||||
reduceAmountByDiff: BigDecimal,
|
||||
notification: Class<out NotificationUM>,
|
||||
)
|
||||
|
||||
fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class<out NotificationUM>)
|
||||
|
||||
fun onNotificationCancel(clazz: Class<out NotificationUM>)
|
||||
// endregion
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="12dp"
|
||||
android:height="7dp"
|
||||
android:viewportWidth="12"
|
||||
android:viewportHeight="7">
|
||||
<path
|
||||
android:pathData="M4.988,0.785C3.819,3.428 1.89,7 0,7H12C10.115,7 8.269,3.447 7.157,0.806C6.784,-0.08 5.376,-0.094 4.988,0.785Z"
|
||||
android:fillColor="#EBEBEB"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue