Updated on 2026-08-14
This commit is contained in:
commit
6ddf9a25ae
20 changed files with 1383 additions and 3 deletions
|
|
@ -24,6 +24,7 @@ sealed class AmountState {
|
|||
* @param appCurrencyCode app currency code
|
||||
* @param isEditingDisabled indicated whether amount is editable
|
||||
* @param reduceAmountBy reduces amount to be sent by specified value
|
||||
* @param isIgnoreReduce ignores reduce amount value
|
||||
*/
|
||||
data class Data(
|
||||
override val isPrimaryButtonEnabled: Boolean,
|
||||
|
|
@ -37,6 +38,7 @@ sealed class AmountState {
|
|||
val appCurrencyCode: String,
|
||||
val isEditingDisabled: Boolean = false,
|
||||
val reduceAmountBy: BigDecimal = BigDecimal.ZERO,
|
||||
val isIgnoreReduce: Boolean = false,
|
||||
) : AmountState()
|
||||
|
||||
data class Empty(
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ dependencies {
|
|||
implementation(deps.compose.material3)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.androidx.paging.runtime)
|
||||
|
||||
/** Other dependencies */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.v2.di
|
|||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel
|
||||
import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel
|
||||
|
|
@ -35,4 +36,9 @@ internal interface SendModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(SendFeeModel::class)
|
||||
fun provideSendFeeModel(model: SendFeeModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SendConfirmModel::class)
|
||||
fun provideSendConfirmModel(model: SendConfirmModel): Model
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.features.send.v2.send.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
/**
|
||||
|
|
@ -23,6 +26,18 @@ internal sealed class SendAnalyticEvents(
|
|||
/** Confirmation screen opened */
|
||||
data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened")
|
||||
|
||||
/** 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,
|
||||
),
|
||||
)
|
||||
|
||||
/** If transaction delays notification is present */
|
||||
data class NoticeTransactionDelays(
|
||||
val token: String,
|
||||
|
|
@ -37,7 +52,40 @@ internal sealed class SendAnalyticEvents(
|
|||
params = mapOf(TOKEN_PARAM to token),
|
||||
)
|
||||
|
||||
/** 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",
|
||||
),
|
||||
)
|
||||
|
||||
/** Share button clicked */
|
||||
data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share")
|
||||
|
||||
/** Expore button clicked */
|
||||
data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore")
|
||||
|
||||
/** Screen reopened from confirmation screen */
|
||||
data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents(
|
||||
event = "Screen Reopened",
|
||||
params = mapOf(SOURCE to source.name),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val SEND_CATEGORY = "Token / Send"
|
||||
}
|
||||
|
||||
internal enum class SendScreenSource {
|
||||
Address,
|
||||
Amount,
|
||||
Fee,
|
||||
Confirm,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.features.send.v2.send.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class SendAnalyticHelper @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) {
|
||||
|
||||
fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) {
|
||||
val destinationUM = sendUM.destinationUM as? DestinationUM.Content
|
||||
val feeUM = sendUM.feeUM as? FeeUM.Content
|
||||
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return
|
||||
val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM)
|
||||
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(destinationUM?.memoTextField),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSendTransactionMemoType(
|
||||
recipientMemo: DestinationTextFieldUM.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,160 @@
|
|||
package com.tangem.features.send.v2.send.confirm
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel
|
||||
import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal class SendConfirmComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: SendConfirmModel = getOrCreateModel(params = params)
|
||||
|
||||
private val blockClickEnableFlow = MutableStateFlow(false)
|
||||
|
||||
// private val destinationBlockComponent =
|
||||
// SendDestinationBlockComponent(
|
||||
// appComponentContext = child("sendConfirmDestinationBlock"),
|
||||
// params = SendDestinationComponentParams.DestinationBlockParams(
|
||||
// state = model.uiState.value.destinationUM,
|
||||
// analyticsCategoryName = params.analyticsCategoryName,
|
||||
// userWallet = params.userWallet,
|
||||
// cryptoCurrency = params.cryptoCurrencyStatus.currency,
|
||||
// blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
|
||||
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
|
||||
// predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address,
|
||||
// predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag,
|
||||
// ),
|
||||
// onResult = model::onDestinationResult,
|
||||
// onClick = model::showEditDestination,
|
||||
// )
|
||||
|
||||
private val amountBlockComponent = SendAmountBlockComponent(
|
||||
appComponentContext = child("sendConfirmAmountBlock"),
|
||||
params = SendAmountComponentParams.AmountBlockParams(
|
||||
state = model.uiState.value.amountUM,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWallet = params.userWallet,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
|
||||
blockEditDisabledFlow = TODO(),
|
||||
// isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content,
|
||||
// predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount,
|
||||
),
|
||||
onResult = model::onAmountResult,
|
||||
onClick = model::showEditAmount,
|
||||
)
|
||||
|
||||
private val feeBlockComponent = SendFeeBlockComponent(
|
||||
appComponentContext = child("sendConfirmFeeBlock"),
|
||||
params = SendFeeComponentParams.FeeBlockParams(
|
||||
state = model.uiState.value.feeUM,
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWallet = params.userWallet,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
sendAmount = model.enteredAmount.orZero(),
|
||||
destinationAddress = model.enteredDestination.orEmpty(),
|
||||
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
onClick = model::showEditFee,
|
||||
)
|
||||
|
||||
private val notificationsComponent = NotificationsComponent(
|
||||
appComponentContext = child("sendConfirmNotifications"),
|
||||
params = NotificationsComponent.Params(
|
||||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
appCurrency = params.appCurrency,
|
||||
destinationAddress = model.enteredDestination.orEmpty(),
|
||||
amountValue = model.enteredAmount.orZero(),
|
||||
reduceAmountBy = model.reduceAmountBy.orZero(),
|
||||
isIgnoreReduce = model.isIgnoreReduce,
|
||||
fee = model.fee,
|
||||
feeError = model.feeError,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
model.uiState.onEach { state ->
|
||||
val confirmUM = state.confirmUM as? ConfirmUM.Content
|
||||
blockClickEnableFlow.value = confirmUM?.isSending == false
|
||||
}.launchIn(componentScope)
|
||||
}
|
||||
|
||||
fun updateState(state: SendUM) {
|
||||
// todo
|
||||
// destinationBlockComponent.updateState(state.destinationUM)
|
||||
amountBlockComponent.updateState(state.amountUM)
|
||||
feeBlockComponent.updateState(state.feeUM)
|
||||
model.updateState(state)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val notificationState by notificationsComponent.state.collectAsStateWithLifecycle()
|
||||
|
||||
SendConfirmContent(
|
||||
sendUM = state,
|
||||
// destinationBlockComponent = destinationBlockComponent,
|
||||
amountBlockComponent = amountBlockComponent,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
notificationsComponent = notificationsComponent,
|
||||
notificationsUM = notificationState,
|
||||
)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val state: SendUM,
|
||||
val analyticsCategoryName: String,
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val appCurrency: AppCurrency,
|
||||
val callback: ModelCallback,
|
||||
val currentRoute: Flow<SendRoute.Confirm>,
|
||||
val isBalanceHidingFlow: StateFlow<Boolean>,
|
||||
val predefinedValues: PredefinedValues,
|
||||
) {
|
||||
sealed class PredefinedValues {
|
||||
data object Empty : PredefinedValues()
|
||||
data class Content(
|
||||
val transactionId: String,
|
||||
val amount: String,
|
||||
val address: String,
|
||||
val tag: String?,
|
||||
) : PredefinedValues()
|
||||
}
|
||||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onResult(sendUM: SendUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model
|
||||
|
||||
import com.tangem.common.ui.alerts.TransactionErrorAlertConverter
|
||||
import com.tangem.common.ui.alerts.models.AlertDemoModeUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class SendConfirmAlertFactory @Inject constructor(
|
||||
private val messageSender: UiMessageSender,
|
||||
) {
|
||||
|
||||
fun getGenericErrorState(onFailedTxEmailClick: () -> Unit) {
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(id = R.string.send_alert_transaction_failed_title),
|
||||
message = resourceReference(id = R.string.common_unknown_error),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_support),
|
||||
onClick = onFailedTxEmailClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun getSendTransactionErrorState(
|
||||
error: SendTransactionError?,
|
||||
popBack: () -> Unit,
|
||||
onFailedTxEmailClick: (String) -> Unit,
|
||||
) {
|
||||
val transactionErrorAlertConverter = TransactionErrorAlertConverter(
|
||||
popBackStack = popBack,
|
||||
onFailedTxEmailClick = onFailedTxEmailClick,
|
||||
)
|
||||
|
||||
val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return
|
||||
val onConfirmClick = errorAlert.onConfirmClick ?: return
|
||||
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = errorAlert.title,
|
||||
message = errorAlert.message,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = errorAlert.confirmButtonText,
|
||||
onClick = onConfirmClick,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = if (errorAlert !is AlertDemoModeUM) {
|
||||
{ cancelAction() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model
|
||||
|
||||
internal interface SendConfirmClickIntents {
|
||||
|
||||
fun showEditDestination()
|
||||
|
||||
fun showEditAmount()
|
||||
|
||||
fun showEditFee()
|
||||
|
||||
fun onSendClick()
|
||||
|
||||
fun onExploreClick()
|
||||
|
||||
fun onShareClick()
|
||||
|
||||
fun onFailedTxEmailClick(errorMessage: String)
|
||||
}
|
||||
|
|
@ -0,0 +1,554 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model
|
||||
|
||||
import android.os.SystemClock
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.feedback.GetCardInfoUseCase
|
||||
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase
|
||||
import com.tangem.domain.settings.NeverShowTapHelpUseCase
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
|
||||
import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase
|
||||
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.features.send.v2.send.SendRoute
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmInitialStateTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData
|
||||
import com.tangem.features.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.DelayedWork
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class SendConfirmModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val router: Router,
|
||||
private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase,
|
||||
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase,
|
||||
private val getCardInfoUseCase: GetCardInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
|
||||
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
|
||||
private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
|
||||
private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase,
|
||||
private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase,
|
||||
private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger,
|
||||
private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter,
|
||||
private val notificationsUpdateTrigger: NotificationsUpdateTrigger,
|
||||
private val alertFactory: SendConfirmAlertFactory,
|
||||
private val sendAnalyticHelper: SendAnalyticHelper,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
@DelayedWork private val coroutineScope: CoroutineScope,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val shareManager: ShareManager,
|
||||
) : Model(), SendConfirmClickIntents {
|
||||
|
||||
private val params: SendConfirmComponent.Params = paramsContainer.require()
|
||||
|
||||
private val userWallet = params.userWallet
|
||||
private val appCurrency = params.appCurrency
|
||||
private val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
private val cryptoCurrency = cryptoCurrencyStatus.currency
|
||||
|
||||
private val _uiState = MutableStateFlow(params.state)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
private val amountState
|
||||
get() = uiState.value.amountUM as? AmountState.Data
|
||||
private val destinationUM
|
||||
get() = uiState.value.destinationUM as? DestinationUM.Content
|
||||
private val feeUM
|
||||
get() = uiState.value.feeUM as? FeeUM.Content
|
||||
private val feeSelectorUM
|
||||
get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
|
||||
|
||||
val enteredAmount: BigDecimal?
|
||||
get() = amountState?.amountTextField?.cryptoAmount?.value
|
||||
val reduceAmountBy: BigDecimal
|
||||
get() = amountState?.reduceAmountBy.orZero()
|
||||
val isIgnoreReduce: Boolean
|
||||
get() = amountState?.isIgnoreReduce == true
|
||||
val enteredDestination: String?
|
||||
get() = destinationUM?.addressTextField?.value
|
||||
val fee: Fee?
|
||||
get() = feeSelectorUM?.selectedFee
|
||||
val feeError: GetFeeError?
|
||||
get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error
|
||||
|
||||
private var sendIdleTimer: Long = 0L
|
||||
private var isAmountSubtractAvailable = false
|
||||
|
||||
init {
|
||||
modelScope.launch {
|
||||
isAmountSubtractAvailable =
|
||||
isAmountSubtractAvailableUseCase(userWallet.walletId, cryptoCurrency).getOrElse { false }
|
||||
}
|
||||
configConfirmNavigation()
|
||||
subscribeOnNotificationsUpdateTrigger()
|
||||
subscribeOnCheckFeeResultUpdates()
|
||||
initialState()
|
||||
}
|
||||
|
||||
fun updateState(state: SendUM) {
|
||||
_uiState.value = state
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
fun onFeeResult(feeUM: FeeUM) {
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
_uiState.update { it.copy(feeUM = feeUM) }
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
fun onAmountResult(amountUM: AmountState) {
|
||||
_uiState.update { it.copy(amountUM = amountUM) }
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
|
||||
override fun showEditDestination() {
|
||||
modelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
_uiState.update {
|
||||
val confirmUM = it.confirmUM as? ConfirmUM.Content
|
||||
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
|
||||
}
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
||||
router.push(SendRoute.Destination(isEditMode = true))
|
||||
}
|
||||
}
|
||||
|
||||
override fun showEditAmount() {
|
||||
modelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
_uiState.update {
|
||||
val confirmUM = it.confirmUM as? ConfirmUM.Content
|
||||
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
|
||||
}
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
||||
router.push(SendRoute.Amount(isEditMode = true))
|
||||
}
|
||||
}
|
||||
|
||||
override fun showEditFee() {
|
||||
modelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
_uiState.update {
|
||||
val confirmUM = it.confirmUM as? ConfirmUM.Content
|
||||
it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM)
|
||||
}
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
||||
router.push(SendRoute.Fee())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSendClick() {
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
|
||||
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
||||
verifyAndSendTransaction()
|
||||
} else {
|
||||
modelScope.launch {
|
||||
sendFeeCheckReloadTrigger.triggerCheckUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExploreClick() {
|
||||
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
||||
urlOpener.openUrl(confirmUM.txUrl)
|
||||
}
|
||||
|
||||
override fun onShareClick() {
|
||||
val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked)
|
||||
shareManager.shareText(confirmUM.txUrl)
|
||||
}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
val amountValue = amountState?.amountTextField?.cryptoAmount?.value
|
||||
val feeValue = fee?.amount?.value
|
||||
|
||||
val receivingAmount = if (amountValue != null && feeValue != null) {
|
||||
checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = enteredAmount.orZero(),
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val amount = receivingAmount?.convertToSdkAmount(cryptoCurrency)
|
||||
|
||||
saveBlockchainErrorUseCase(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = errorMessage,
|
||||
blockchainId = cryptoCurrency.network.id.value,
|
||||
derivationPath = cryptoCurrency.network.derivationPath.value,
|
||||
destinationAddress = enteredDestination.orEmpty(),
|
||||
tokenSymbol = if (amount?.type is AmountType.Token) {
|
||||
amount.currencySymbol
|
||||
} else {
|
||||
""
|
||||
},
|
||||
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
|
||||
fee = feeValue?.convertToSdkAmount(cryptoCurrency)
|
||||
?.value?.stripZeroPlainString() ?: "unknown",
|
||||
),
|
||||
)
|
||||
|
||||
val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return
|
||||
|
||||
modelScope.launch {
|
||||
sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo))
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState() {
|
||||
val confirmUM = uiState.value.confirmUM
|
||||
val amountUM = uiState.value.amountUM
|
||||
val feeUM = uiState.value.feeUM
|
||||
|
||||
modelScope.launch {
|
||||
val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false }
|
||||
if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
confirmUM = SendConfirmInitialStateTransformer(
|
||||
appCurrency = appCurrency,
|
||||
feeUM = feeUM,
|
||||
amountUM = amountUM,
|
||||
isShowTapHelp = isShowTapHelp,
|
||||
isSubtracted = false,
|
||||
).transform(uiState.value.confirmUM),
|
||||
)
|
||||
}
|
||||
updateConfirmNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnNotificationsUpdateTrigger() {
|
||||
notificationsUpdateTrigger.hasErrorFlow
|
||||
.onEach { hasError ->
|
||||
_uiState.update {
|
||||
val feeUM = it.feeUM as? FeeUM.Content
|
||||
val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content
|
||||
it.copy(
|
||||
confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy(
|
||||
isPrimaryButtonEnabled = !hasError && feeSelectorUM != null,
|
||||
) ?: it.confirmUM,
|
||||
)
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction() {
|
||||
val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val destination = destinationUM?.addressTextField?.value ?: return
|
||||
val memo = destinationUM?.memoTextField?.value
|
||||
val fee = feeSelectorUM?.selectedFee
|
||||
val feeValue = fee?.amount?.value ?: return
|
||||
|
||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = reduceAmountBy.orZero(),
|
||||
)
|
||||
|
||||
modelScope.launch {
|
||||
createTransactionUseCase(
|
||||
amount = receivingAmount.convertToSdkAmount(cryptoCurrency),
|
||||
fee = fee,
|
||||
memo = memo,
|
||||
destination = destination,
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e(error)
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
|
||||
alertFactory.getGenericErrorState {
|
||||
onFailedTxEmailClick(error.localizedMessage.orEmpty())
|
||||
}
|
||||
},
|
||||
ifRight = { txData ->
|
||||
sendTransaction(txData)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
||||
val result = sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
|
||||
|
||||
result.fold(
|
||||
ifLeft = { error ->
|
||||
alertFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
popBack = appRouter::pop,
|
||||
onFailedTxEmailClick = ::onFailedTxEmailClick,
|
||||
)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
|
||||
},
|
||||
ifRight = {
|
||||
updateTransactionStatus(txData)
|
||||
addTokenToWalletIfNeeded()
|
||||
scheduleUpdates()
|
||||
sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun addTokenToWalletIfNeeded() {
|
||||
if (cryptoCurrency !is CryptoCurrency.Token) return
|
||||
val wallets = destinationUM?.wallets ?: return
|
||||
|
||||
val receivingUserWallet = wallets
|
||||
.firstOrNull { it.address == enteredDestination }
|
||||
?: return
|
||||
|
||||
val userWalletId = receivingUserWallet.userWalletId ?: return
|
||||
val network = receivingUserWallet.network ?: return
|
||||
|
||||
modelScope.launch {
|
||||
addCryptoCurrenciesUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
network = network,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) {
|
||||
val txUrl = getExplorerTransactionUrlUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrElse { "" }
|
||||
_uiState.update(SendConfirmSentStateTransformer(txData, txUrl))
|
||||
}
|
||||
|
||||
private fun scheduleUpdates() {
|
||||
coroutineScope.launch {
|
||||
listOf(
|
||||
// we should update network to find pending tx after 1 sec
|
||||
async {
|
||||
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network))
|
||||
},
|
||||
// we should update tx history and network for new balance
|
||||
async {
|
||||
updateTxHistory()
|
||||
},
|
||||
async {
|
||||
updateDelayedCurrencyStatusUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrency.network,
|
||||
delayMillis = BALANCE_UPDATE_DELAY,
|
||||
refresh = true,
|
||||
)
|
||||
},
|
||||
).awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateTxHistory() {
|
||||
delay(BALANCE_UPDATE_DELAY)
|
||||
val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
|
||||
txHistoryItemsCountEither.onRight {
|
||||
if (txHistoryFeatureToggles.isFeatureEnabled) {
|
||||
txHistoryContentUpdateEmitter.triggerUpdate()
|
||||
} else {
|
||||
getTxHistoryItemsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = cryptoCurrency,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeOnCheckFeeResultUpdates() {
|
||||
sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess ->
|
||||
if (isFeeResultSuccess) {
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = true))
|
||||
verifyAndSendTransaction()
|
||||
} else {
|
||||
_uiState.update(SendConfirmSendingStateTransformer(isSending = false))
|
||||
}
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateConfirmNotifications() {
|
||||
modelScope.launch {
|
||||
notificationsUpdateTrigger.triggerUpdate(
|
||||
data = NotificationData(
|
||||
destinationAddress = enteredDestination.orEmpty(),
|
||||
amountValue = enteredAmount.orZero(),
|
||||
reduceAmountBy = reduceAmountBy.orZero(),
|
||||
isIgnoreReduce = isIgnoreReduce,
|
||||
fee = fee,
|
||||
feeError = feeError,
|
||||
),
|
||||
)
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
confirmUM = SendConfirmationNotificationsTransformer(
|
||||
feeUM = uiState.value.feeUM,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
).transform(uiState.value.confirmUM),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun configConfirmNavigation() {
|
||||
combine(
|
||||
flow = uiState,
|
||||
flow2 = params.currentRoute,
|
||||
transform = { state, route -> state to route },
|
||||
).onEach { (state, _) ->
|
||||
// todo
|
||||
// val amountUM = state.amountUM as? AmountState.Data
|
||||
// val confirmUM = state.confirmUM
|
||||
// params.callback.onResult(
|
||||
// state.copy(
|
||||
// navigationUM = NavigationUM.Content(
|
||||
// title = resourceReference(
|
||||
// id = R.string.send_summary_title,
|
||||
// formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
|
||||
// ),
|
||||
// subtitle = amountUM?.title,
|
||||
// backIconRes = R.drawable.ic_close_24,
|
||||
// backIconClick = {
|
||||
// analyticsEventHandler.send(
|
||||
// SendAnalyticEvents.CloseButtonClicked(
|
||||
// source = SendScreenSource.Confirm,
|
||||
// isFromSummary = true,
|
||||
// isValid = confirmUM.isPrimaryButtonEnabled,
|
||||
// ),
|
||||
// )
|
||||
// appRouter.pop()
|
||||
// },
|
||||
// primaryButton = ButtonsUM.PrimaryButtonUM(
|
||||
// text = when (confirmUM) {
|
||||
// is ConfirmUM.Success -> resourceReference(R.string.common_close)
|
||||
// is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
// resourceReference(R.string.send_sending)
|
||||
// } else {
|
||||
// resourceReference(R.string.common_send)
|
||||
// }
|
||||
// else -> resourceReference(R.string.common_send)
|
||||
// },
|
||||
// iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content },
|
||||
// isEnabled = confirmUM.isPrimaryButtonEnabled,
|
||||
// isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending,
|
||||
// onClick = {
|
||||
// when (confirmUM) {
|
||||
// is ConfirmUM.Success -> appRouter.pop()
|
||||
// is ConfirmUM.Content -> if (confirmUM.isSending) {
|
||||
// return@PrimaryButtonUM
|
||||
// } else {
|
||||
// onSendClick()
|
||||
// }
|
||||
// else -> return@PrimaryButtonUM
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// prevButton = null,
|
||||
// secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM(
|
||||
// leftText = resourceReference(R.string.common_explore),
|
||||
// leftIconResId = R.drawable.ic_web_24,
|
||||
// onLeftClick = ::onExploreClick,
|
||||
// rightText = resourceReference(R.string.common_share),
|
||||
// rightIconResId = R.drawable.ic_share_24,
|
||||
// onRightClick = ::onShareClick,
|
||||
// ).takeIf { confirmUM is ConfirmUM.Success },
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CHECK_FEE_UPDATE_DELAY = 10_000L
|
||||
const val BALANCE_UPDATE_DELAY = 11_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
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.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
internal class SendConfirmInitialStateTransformer(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val feeUM: FeeUM,
|
||||
private val amountUM: AmountState,
|
||||
private val isShowTapHelp: Boolean,
|
||||
private val isSubtracted: Boolean,
|
||||
) : Transformer<ConfirmUM> {
|
||||
override fun transform(prevState: ConfirmUM): ConfirmUM {
|
||||
return ConfirmUM.Content(
|
||||
isSending = false,
|
||||
showTapHelp = isShowTapHelp,
|
||||
sendingFooter = getSendingFooterText(),
|
||||
notifications = persistentListOf(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSendingFooterText(): TextReference {
|
||||
val feeUM = feeUM as? FeeUM.Content
|
||||
val amountUM = amountUM as? AmountState.Data
|
||||
|
||||
if (feeUM == null || amountUM == null) return TextReference.EMPTY
|
||||
|
||||
val fee = (feeUM.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee
|
||||
val fiatAmountValue = amountUM.amountTextField.fiatAmount.value
|
||||
val fiatFeeValue = feeUM.rate?.let { fee?.amount?.value?.multiply(it) }
|
||||
|
||||
val fiatSendingValue = if (isSubtracted) {
|
||||
fiatAmountValue
|
||||
} else {
|
||||
if (feeUM.isFeeConvertibleToFiat) {
|
||||
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
|
||||
} else {
|
||||
fiatAmountValue
|
||||
}
|
||||
}
|
||||
|
||||
val fiatSending = fiatSendingValue.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
val fiatFee = formatFiatFee(
|
||||
amount = fee?.amount,
|
||||
isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat,
|
||||
isFeeApproximate = feeUM.isFeeApproximate,
|
||||
)
|
||||
|
||||
return if (feeUM.isTronToken && fee is Fee.Tron) {
|
||||
getTokenFeeSendingText(
|
||||
fee = fee,
|
||||
fiatFee = fiatFee,
|
||||
fiatSending = fiatSending,
|
||||
)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = if (feeUM.isFeeConvertibleToFiat) {
|
||||
R.string.send_summary_transaction_description
|
||||
} else {
|
||||
R.string.send_summary_transaction_description_no_fiat_fee
|
||||
},
|
||||
formatArgs = wrappedList(fiatSending, fiatFee),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String {
|
||||
return if (isFeeConvertibleToFiat) {
|
||||
amount?.value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
amount?.value.format {
|
||||
crypto(
|
||||
decimals = amount?.decimals ?: 0,
|
||||
symbol = amount?.currencySymbol.orEmpty(),
|
||||
).fee(
|
||||
canBeLower = isFeeApproximate,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference {
|
||||
val suffix = when {
|
||||
fee.remainingEnergy == 0L -> {
|
||||
resourceReference(
|
||||
R.string.send_summary_transaction_description_suffix_including,
|
||||
wrappedList(fiatFee),
|
||||
)
|
||||
}
|
||||
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(fiatSending),
|
||||
)
|
||||
|
||||
return combinedReference(prefix, COMMA_SEPARATOR, suffix)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val COMMA_SEPARATOR = stringReference(", ")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendConfirmSendingStateTransformer(
|
||||
val isSending: Boolean,
|
||||
) : Transformer<SendUM> {
|
||||
override fun transform(prevState: SendUM): SendUM {
|
||||
val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState
|
||||
return prevState.copy(
|
||||
confirmUM = confirmUM.copy(
|
||||
isPrimaryButtonEnabled = !isSending,
|
||||
isSending = isSending,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
|
||||
internal class SendConfirmSentStateTransformer(
|
||||
val txData: TransactionData.Uncompiled,
|
||||
val txUrl: String,
|
||||
) : Transformer<SendUM> {
|
||||
override fun transform(prevState: SendUM): SendUM {
|
||||
return prevState.copy(
|
||||
confirmUM = ConfirmUM.Success(
|
||||
transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(),
|
||||
txUrl = txUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.features.send.v2.send.confirm.model.transformers
|
||||
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class SendConfirmationNotificationsTransformer(
|
||||
private val feeUM: FeeUM,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
) : Transformer<ConfirmUM> {
|
||||
override fun transform(prevState: ConfirmUM): ConfirmUM {
|
||||
val state = prevState as? ConfirmUM.Content ?: return prevState
|
||||
val feeUM = feeUM as? FeeUM.Content ?: return prevState
|
||||
return state.copy(
|
||||
notifications = buildList {
|
||||
addTooHighNotification(feeUM.feeSelectorUM)
|
||||
addTooLowNotification(feeUM)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooLowNotification(feeUM: FeeUM.Content) {
|
||||
val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
|
||||
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return
|
||||
val minimumValue = multipleFees.minimum.amount.value ?: return
|
||||
val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return
|
||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||
if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) {
|
||||
add(NotificationUM.Warning.FeeTooLow)
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM) {
|
||||
if (feeSelectorUM !is FeeSelectorUM.Content) return
|
||||
|
||||
val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM)
|
||||
if (isFeeTooHigh) {
|
||||
add(NotificationUM.Warning.TooHigh(diff))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
package com.tangem.features.send.v2.send.confirm.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
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.utils.DateTimeFormatters
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent
|
||||
import com.tangem.features.send.v2.subcomponents.notifications
|
||||
import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val TAP_HELP_KEY = "TAP_HELP_KEY"
|
||||
private const val BLOCKS_KEY = "BLOCKS_KEY"
|
||||
private const val TAP_HELP_ANIMATION_DELAY = 500L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun SendConfirmContent(
|
||||
sendUM: SendUM,
|
||||
// destinationBlockComponent: SendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
notificationsComponent: NotificationsComponent,
|
||||
notificationsUM: ImmutableList<NotificationUM>,
|
||||
) {
|
||||
val confirmUM = sendUM.confirmUM as? ConfirmUM.Content
|
||||
|
||||
Column {
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
blocks(
|
||||
uiState = sendUM,
|
||||
// destinationBlockComponent = destinationBlockComponent,
|
||||
amountBlockComponent = amountBlockComponent,
|
||||
feeBlockComponent = feeBlockComponent,
|
||||
)
|
||||
if (confirmUM != null) {
|
||||
tapHelp(isDisplay = confirmUM.showTapHelp)
|
||||
with(notificationsComponent) {
|
||||
content(
|
||||
state = notificationsUM,
|
||||
isClickDisabled = confirmUM.isSending,
|
||||
)
|
||||
}
|
||||
notifications(
|
||||
notifications = confirmUM.notifications,
|
||||
isClickDisabled = confirmUM.isSending,
|
||||
)
|
||||
}
|
||||
}
|
||||
SendingText(confirmUM = confirmUM)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modifier) {
|
||||
var isVisibleProxy by remember { mutableStateOf(confirmUM != null) }
|
||||
val keyboard by keyboardAsState()
|
||||
|
||||
// the text should appear when the keyboard is closed
|
||||
LaunchedEffect(confirmUM != null, keyboard) {
|
||||
if (confirmUM != null && keyboard is Keyboard.Opened) {
|
||||
return@LaunchedEffect
|
||||
}
|
||||
isVisibleProxy = confirmUM != null
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisibleProxy,
|
||||
modifier = modifier,
|
||||
enter = slideInVertically() + fadeIn(),
|
||||
exit = fadeOut(tween(durationMillis = 300)),
|
||||
label = "Animate show sending state text",
|
||||
) {
|
||||
val wrappedConfirmUM = remember(this) { requireNotNull(confirmUM?.sendingFooter) }
|
||||
Text(
|
||||
text = wrappedConfirmUM.resolveAnnotatedReference(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.blocks(
|
||||
uiState: SendUM,
|
||||
// destinationBlockComponent: SendDestinationBlockComponent,
|
||||
amountBlockComponent: SendAmountBlockComponent,
|
||||
feeBlockComponent: SendFeeBlockComponent,
|
||||
) {
|
||||
item(key = BLOCKS_KEY) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
AnimatedVisibility(
|
||||
visible = uiState.confirmUM is ConfirmUM.Success,
|
||||
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success }
|
||||
TransactionDoneTitle(
|
||||
title = resourceReference(R.string.sent_transaction_sent_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_date_format,
|
||||
wrappedList(
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||
wrappedConfirmUM.transactionDate.toTimeFormat(),
|
||||
),
|
||||
),
|
||||
modifier = Modifier.padding(vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
// todo
|
||||
// destinationBlockComponent.Content(modifier = Modifier)
|
||||
amountBlockComponent.Content(modifier = Modifier)
|
||||
feeBlockComponent.Content(modifier = Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) {
|
||||
item(key = TAP_HELP_KEY) {
|
||||
var wrappedIsDisplay by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = isDisplay) {
|
||||
delay(TAP_HELP_ANIMATION_DELAY)
|
||||
wrappedIsDisplay = isDisplay
|
||||
}
|
||||
|
||||
if (wrappedIsDisplay) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem()
|
||||
.padding(top = TangemTheme.dimens.spacing20),
|
||||
) {
|
||||
val background = TangemTheme.colors.button.secondary
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_send_hint_shape_12),
|
||||
tint = TangemTheme.colors.button.secondary,
|
||||
contentDescription = null,
|
||||
modifier = Modifier,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.send_summary_tap_hint),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(background)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing14,
|
||||
vertical = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.features.send.v2.send.confirm.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
internal sealed class ConfirmUM {
|
||||
|
||||
abstract val isPrimaryButtonEnabled: Boolean
|
||||
|
||||
data class Content(
|
||||
override val isPrimaryButtonEnabled: Boolean = false,
|
||||
val isSending: Boolean,
|
||||
val showTapHelp: Boolean,
|
||||
val sendingFooter: TextReference,
|
||||
val notifications: ImmutableList<NotificationUM>,
|
||||
) : ConfirmUM()
|
||||
|
||||
data class Success(
|
||||
val transactionDate: Long,
|
||||
val txUrl: String,
|
||||
) : ConfirmUM() {
|
||||
override val isPrimaryButtonEnabled: Boolean = true
|
||||
}
|
||||
|
||||
data object Empty : ConfirmUM() {
|
||||
override val isPrimaryButtonEnabled: Boolean = false
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.send.ui.state.SendUM
|
||||
import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent
|
||||
import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent
|
||||
|
|
@ -28,7 +30,8 @@ internal class SendModel @Inject constructor(
|
|||
) : Model(),
|
||||
SendDestinationComponent.ModelCallback,
|
||||
SendAmountComponent.ModelCallback,
|
||||
SendFeeComponent.ModelCallback {
|
||||
SendFeeComponent.ModelCallback,
|
||||
SendConfirmComponent.ModelCallback {
|
||||
|
||||
private val _uiState = MutableStateFlow(initialState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
|
@ -39,6 +42,10 @@ internal class SendModel @Inject constructor(
|
|||
var appCurrency: AppCurrency = AppCurrency.Default
|
||||
var predefinedAmountValue: String? = null
|
||||
|
||||
override fun onResult(sendUM: SendUM) {
|
||||
_uiState.value = sendUM
|
||||
}
|
||||
|
||||
override fun onDestinationResult(destinationUM: DestinationUM) {
|
||||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
|
@ -55,5 +62,6 @@ internal class SendModel @Inject constructor(
|
|||
amountUM = AmountState.Empty(),
|
||||
destinationUM = DestinationUM.Empty(),
|
||||
feeUM = FeeUM.Empty(),
|
||||
confirmUM = ConfirmUM.Empty,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.send.v2.send.ui.state
|
||||
|
||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||
import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
|
||||
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
|
||||
|
||||
|
|
@ -8,4 +9,5 @@ internal data class SendUM(
|
|||
val amountUM: AmountState,
|
||||
val destinationUM: DestinationUM,
|
||||
val feeUM: FeeUM,
|
||||
val confirmUM: ConfirmUM,
|
||||
)
|
||||
|
|
@ -25,6 +25,6 @@ internal class SendFeeComponent(
|
|||
}
|
||||
|
||||
interface ModelCallback {
|
||||
fun onFeeResult(state: FeeUM)
|
||||
fun onFeeResult(feeUM: FeeUM)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ import javax.inject.Inject
|
|||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
class NotificationsModel @Inject constructor(
|
||||
internal class NotificationsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appRouter: AppRouter,
|
||||
|
|
|
|||
|
|
@ -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:fillColor="#EBEBEB"
|
||||
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" />
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue