Updated on 2026-08-14
This commit is contained in:
parent
385cc79a32
commit
a3f2c421bf
69 changed files with 749 additions and 542 deletions
|
|
@ -2,8 +2,8 @@ package com.tangem.tap.common.analytics.converters
|
||||||
|
|
||||||
import com.tangem.blockchain.common.BlockchainSdkError
|
import com.tangem.blockchain.common.BlockchainSdkError
|
||||||
import com.tangem.common.core.TangemSdkError
|
import com.tangem.common.core.TangemSdkError
|
||||||
|
import com.tangem.domain.demo.DemoTransactionSender
|
||||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.DemoConfig
|
||||||
import com.tangem.domain.transaction.FeeRepository
|
import com.tangem.domain.transaction.FeeRepository
|
||||||
import com.tangem.domain.transaction.TransactionRepository
|
import com.tangem.domain.transaction.TransactionRepository
|
||||||
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
|
||||||
|
|
@ -22,20 +22,24 @@ internal object TransactionDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
fun provideGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetFeeUseCase {
|
||||||
return GetFeeUseCase(walletManagersFacade)
|
return GetFeeUseCase(
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
demoConfig = DemoConfig(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideSendTransactionUseCase(
|
fun provideSendTransactionUseCase(
|
||||||
isDemoCardUseCase: IsDemoCardUseCase,
|
|
||||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
transactionRepository: TransactionRepository,
|
transactionRepository: TransactionRepository,
|
||||||
|
walletManagersFacade: WalletManagersFacade,
|
||||||
): SendTransactionUseCase {
|
): SendTransactionUseCase {
|
||||||
return SendTransactionUseCase(
|
return SendTransactionUseCase(
|
||||||
isDemoCardUseCase = isDemoCardUseCase,
|
demoConfig = DemoConfig(),
|
||||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||||
transactionRepository = transactionRepository,
|
transactionRepository = transactionRepository,
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
|
||||||
import com.tangem.blockchain.extensions.Result
|
import com.tangem.blockchain.extensions.Result
|
||||||
import com.tangem.common.extensions.isZero
|
import com.tangem.common.extensions.isZero
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
import com.tangem.domain.demo.DemoTransactionSender
|
||||||
import com.tangem.tap.features.demo.isDemoCard
|
import com.tangem.tap.features.demo.isDemoCard
|
||||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||||
import com.tangem.tap.features.send.redux.FeeAction
|
import com.tangem.tap.features.send.redux.FeeAction
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
||||||
import com.tangem.tap.domain.TangemSigner
|
import com.tangem.tap.domain.TangemSigner
|
||||||
import com.tangem.tap.domain.TapError
|
import com.tangem.tap.domain.TapError
|
||||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||||
import com.tangem.tap.features.demo.DemoTransactionSender
|
import com.tangem.domain.demo.DemoTransactionSender
|
||||||
import com.tangem.tap.features.demo.isDemoCard
|
import com.tangem.tap.features.demo.isDemoCard
|
||||||
import com.tangem.tap.features.send.redux.*
|
import com.tangem.tap.features.send.redux.*
|
||||||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import androidx.core.view.postDelayed
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
|
|
@ -54,14 +54,13 @@ import com.tangem.tap.features.send.redux.states.FeeType
|
||||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||||
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
|
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
|
||||||
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
||||||
import com.tangem.tap.mainScope
|
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import com.tangem.wallet.databinding.FragmentSendBinding
|
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -73,6 +72,7 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
||||||
/**
|
/**
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LargeClass")
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
|
|
@ -103,18 +103,25 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
lifecycle.addObserver(viewModel)
|
lifecycle.addObserver(viewModel)
|
||||||
sendSubscriber.initViewModel(viewModel)
|
sendSubscriber.initViewModel(viewModel)
|
||||||
Analytics.send(Token.Send.ScreenOpened())
|
Analytics.send(Token.Send.ScreenOpened())
|
||||||
listenToQrCode()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
subscribeToQrCodeScanner()
|
||||||
|
subscribeToTransactionExtrasFields()
|
||||||
|
subscribeToAddressField()
|
||||||
|
subscribeToAmountField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addBackPressHandler(this)
|
addBackPressHandler(this)
|
||||||
|
|
||||||
etAmountToSend = view.findViewById(R.id.etAmountToSend)
|
etAmountToSend = view.findViewById(R.id.etAmountToSend)
|
||||||
|
|
||||||
initSendButtonStates()
|
initSendButtonStates()
|
||||||
setupAddressLayout()
|
setupAddressLayout()
|
||||||
setupTransactionExtrasLayout()
|
|
||||||
setupAmountLayout()
|
setupAmountLayout()
|
||||||
setupFeeLayout()
|
setupFeeLayout()
|
||||||
setupWarningMessages()
|
setupWarningMessages()
|
||||||
|
|
@ -149,14 +156,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
setOnFocusChangeListener { _, hasFocus ->
|
setOnFocusChangeListener { _, hasFocus ->
|
||||||
store.dispatch(TruncateOrRestore(!hasFocus))
|
store.dispatch(TruncateOrRestore(!hasFocus))
|
||||||
}
|
}
|
||||||
|
|
||||||
inputtedTextAsFlow()
|
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
|
||||||
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
|
|
||||||
.onEach {
|
|
||||||
store.dispatch(AddressActionUi.HandleUserInput(it))
|
|
||||||
}
|
|
||||||
.launchIn(mainScope)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
imvPaste.setOnClickListener {
|
imvPaste.setOnClickListener {
|
||||||
|
|
@ -183,31 +182,41 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenToQrCode() {
|
private fun CoroutineScope.subscribeToAddressField() = with(binding.lSendAddress) {
|
||||||
lifecycleScope.launch {
|
etAddress.inputtedTextAsFlow()
|
||||||
listenToQrScanningUseCase(SourceType.SEND)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
.getOrElse { emptyFlow() }
|
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
|
||||||
.flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED)
|
.onEach {
|
||||||
.collect { rawQr ->
|
store.dispatch(AddressActionUi.HandleUserInput(it))
|
||||||
delay(200)
|
}
|
||||||
|
.launchIn(this@subscribeToAddressField)
|
||||||
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
|
|
||||||
// If do not use the delay, then etAmount error field is not displayed when
|
|
||||||
// inserting an incorrect amount by shareUri
|
|
||||||
cryptoCurrency?.let { cryptoCurrency ->
|
|
||||||
parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold(
|
|
||||||
ifLeft = {
|
|
||||||
onCodeScanned(QrResult(address = rawQr))
|
|
||||||
Timber.w(it)
|
|
||||||
},
|
|
||||||
ifRight = { onCodeScanned(it) },
|
|
||||||
)
|
|
||||||
} ?: onCodeScanned(QrResult(address = rawQr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
|
private fun CoroutineScope.subscribeToAmountField() {
|
||||||
|
etAmountToSend.inputtedTextAsFlow()
|
||||||
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
|
||||||
|
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
|
||||||
|
.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.subscribeToQrCodeScanner() {
|
||||||
|
listenToQrScanningUseCase(SourceType.SEND)
|
||||||
|
.getOrElse { emptyFlow() }
|
||||||
|
.onEach { rawQr ->
|
||||||
|
cryptoCurrency?.let { cryptoCurrency ->
|
||||||
|
parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold(
|
||||||
|
ifLeft = {
|
||||||
|
onCodeScanned(QrResult(address = rawQr))
|
||||||
|
Timber.w(it)
|
||||||
|
},
|
||||||
|
ifRight = { onCodeScanned(it) },
|
||||||
|
)
|
||||||
|
} ?: onCodeScanned(QrResult(address = rawQr))
|
||||||
|
}.launchIn(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) {
|
||||||
// TODO: [REDACTED_TASK_KEY]
|
// TODO: [REDACTED_TASK_KEY]
|
||||||
etXlmMemo.inputtedTextAsFlow()
|
etXlmMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -216,7 +225,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.xlmMemo?.viewFieldValue?.value != it
|
info.xlmMemo?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etDestinationTag.inputtedTextAsFlow()
|
etDestinationTag.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -225,7 +234,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.xrpDestinationTag?.viewFieldValue?.value != it
|
info.xrpDestinationTag?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etBinanceMemo.inputtedTextAsFlow()
|
etBinanceMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -234,7 +243,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.binanceMemo?.viewFieldValue?.value != it
|
info.binanceMemo?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etTonMemo.inputtedTextAsFlow()
|
etTonMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -243,7 +252,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.tonMemoState?.viewFieldValue?.value != it
|
info.tonMemoState?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etCosmosMemo.inputtedTextAsFlow()
|
etCosmosMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -252,7 +261,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.cosmosMemoState?.viewFieldValue?.value != it
|
info.cosmosMemoState?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etHederaMemo.inputtedTextAsFlow()
|
etHederaMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -261,7 +270,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.hederaMemoState?.viewFieldValue?.value != it
|
info.hederaMemoState?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
|
|
||||||
etAlgorandMemo.inputtedTextAsFlow()
|
etAlgorandMemo.inputtedTextAsFlow()
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
||||||
|
|
@ -270,7 +279,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
info.algorandMemoState?.viewFieldValue?.value != it
|
info.algorandMemoState?.viewFieldValue?.value != it
|
||||||
}
|
}
|
||||||
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
|
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
|
||||||
.launchIn(mainScope)
|
.launchIn(this@subscribeToTransactionExtrasFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCodeScanned(parsedQr: QrResult) {
|
private fun onCodeScanned(parsedQr: QrResult) {
|
||||||
|
|
@ -343,12 +352,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||||
if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0")
|
if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0")
|
||||||
}
|
}
|
||||||
|
|
||||||
etAmountToSend.inputtedTextAsFlow()
|
|
||||||
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
|
|
||||||
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
|
|
||||||
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
|
|
||||||
.launchIn(mainScope)
|
|
||||||
|
|
||||||
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
|
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
|
||||||
it.hideSoftKeyboard()
|
it.hideSoftKeyboard()
|
||||||
it.clearFocus()
|
it.clearFocus()
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ class MoonPayService(
|
||||||
.appendQueryParameter("apiKey", apiKey)
|
.appendQueryParameter("apiKey", apiKey)
|
||||||
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
|
.appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase())
|
||||||
.appendQueryParameter("refundWalletAddress", walletAddress)
|
.appendQueryParameter("refundWalletAddress", walletAddress)
|
||||||
.appendQueryParameter("redirectURL", "tangem://redirect_sell")
|
.appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}")
|
||||||
|
|
||||||
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
|
if (isDarkTheme) uri.appendQueryParameter("theme", "dark")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
||||||
transactionId = params["transactionId"] ?: return,
|
transactionId = params["transactionId"] ?: return,
|
||||||
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
baseCurrencyAmount = params["baseCurrencyAmount"] ?: return,
|
||||||
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
depositWalletAddress = params["depositWalletAddress"] ?: return,
|
||||||
|
currencyId = params["currency_id"] ?: return,
|
||||||
depositWalletAddressTag = params["depositWalletAddressTag"],
|
depositWalletAddressTag = params["depositWalletAddressTag"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,6 +22,7 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink {
|
||||||
val transactionId: String,
|
val transactionId: String,
|
||||||
val baseCurrencyAmount: String,
|
val baseCurrencyAmount: String,
|
||||||
val depositWalletAddress: String,
|
val depositWalletAddress: String,
|
||||||
|
val currencyId: String,
|
||||||
val depositWalletAddressTag: String?,
|
val depositWalletAddressTag: String?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||||
"version": "5.9.0"
|
"version": "5.9.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LOCAL_USER_LOGS_ENABLED",
|
"name": "LOCAL_USER_LOGS_ENABLED",
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@
|
||||||
<string name="add_tokens_title">Валюты</string>
|
<string name="add_tokens_title">Валюты</string>
|
||||||
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
|
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
|
||||||
<string name="alert_button_request_support">Обратиться в поддержку</string>
|
<string name="alert_button_request_support">Обратиться в поддержку</string>
|
||||||
|
<string name="alert_button_try_again">Попробовать снова</string>
|
||||||
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
|
<string name="alert_demo_feature_disabled">Эта функция недоступна в демонстрационном режиме</string>
|
||||||
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
|
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
|
||||||
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
|
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
|
||||||
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
|
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
|
||||||
<string name="alert_manage_tokens_unsupported_curve_message">Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.</string>
|
<string name="alert_manage_tokens_unsupported_curve_message">Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.</string>
|
||||||
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
|
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
|
||||||
|
<string name="alert_negative_app_rate_sent_message">Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки.</string>
|
||||||
|
<string name="alert_negative_app_rate_sent_title">Ваши предложения отправлены</string>
|
||||||
|
<string name="alert_troubleshooting_scan_card_message">Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, или запросите поддержку.</string>
|
||||||
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
|
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
|
||||||
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
|
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
|
||||||
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
|
<string name="app_settings_default_fee_footer">Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться.</string>
|
||||||
|
|
@ -24,6 +28,8 @@
|
||||||
<string name="app_settings_theme_mode_dark">Тёмная</string>
|
<string name="app_settings_theme_mode_dark">Тёмная</string>
|
||||||
<string name="app_settings_theme_mode_light">Светлая</string>
|
<string name="app_settings_theme_mode_light">Светлая</string>
|
||||||
<string name="app_settings_theme_mode_system">Как в системе</string>
|
<string name="app_settings_theme_mode_system">Как в системе</string>
|
||||||
|
<string name="app_settings_theme_selection_footer">При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства</string>
|
||||||
|
<string name="app_settings_theme_selection_system_short">Системная</string>
|
||||||
<string name="app_settings_theme_selector_title">Тема</string>
|
<string name="app_settings_theme_selector_title">Тема</string>
|
||||||
<string name="app_settings_title">Настройки приложения</string>
|
<string name="app_settings_title">Настройки приложения</string>
|
||||||
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
|
<string name="balance_hidden_description">Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\"</string>
|
||||||
|
|
@ -51,6 +57,10 @@
|
||||||
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
|
<string name="card_settings_reset_card_to_factory">Заводские настройки</string>
|
||||||
<string name="card_settings_security_mode">Тип безопасности</string>
|
<string name="card_settings_security_mode">Тип безопасности</string>
|
||||||
<string name="card_settings_title">Настройки карты</string>
|
<string name="card_settings_title">Настройки карты</string>
|
||||||
|
<string name="cardano_coin_will_be_send_with_token_description">Ввиду особенностей сети Cardano при транзакции токена %1$s помимо комиссии сети будет списано %2$s</string>
|
||||||
|
<string name="cardano_insufficient_balance_to_send_token_description">Для совершения транзакции %1$s, вам необходимо внести немного %2$s (%3$s), чтобы покрыть комиссию сети и минимальное значение ADA для отправки.</string>
|
||||||
|
<string name="cardano_insufficient_balance_to_send_token_title">Недостаточно ADA для отправки токена</string>
|
||||||
|
<string name="cardano_max_amount_has_token_description">Вывод всего баланса ADA невозможен при наличии средств на токенах сети Cardano. Сначала выведите средства на ваших токенах.</string>
|
||||||
<string name="common_accept">Принять</string>
|
<string name="common_accept">Принять</string>
|
||||||
<string name="common_access_denied">Доступ запрещен</string>
|
<string name="common_access_denied">Доступ запрещен</string>
|
||||||
<string name="common_apply">Применить</string>
|
<string name="common_apply">Применить</string>
|
||||||
|
|
@ -71,6 +81,7 @@
|
||||||
<string name="common_create">Создать</string>
|
<string name="common_create">Создать</string>
|
||||||
<string name="common_delete">Удалить</string>
|
<string name="common_delete">Удалить</string>
|
||||||
<string name="common_disabled">Отключено</string>
|
<string name="common_disabled">Отключено</string>
|
||||||
|
<string name="common_disconnect">Отключить</string>
|
||||||
<string name="common_done">Готово</string>
|
<string name="common_done">Готово</string>
|
||||||
<string name="common_enable">Включить</string>
|
<string name="common_enable">Включить</string>
|
||||||
<string name="common_enabled">Включено</string>
|
<string name="common_enabled">Включено</string>
|
||||||
|
|
@ -96,12 +107,14 @@
|
||||||
<string name="common_no_address">Нет адреса</string>
|
<string name="common_no_address">Нет адреса</string>
|
||||||
<string name="common_ok">OK</string>
|
<string name="common_ok">OK</string>
|
||||||
<string name="common_origin_card">Основная карта</string>
|
<string name="common_origin_card">Основная карта</string>
|
||||||
<string name="common_passphrase">Кодовая фраза</string>
|
<string name="common_passphrase">Парольная фраза</string>
|
||||||
|
<string name="common_paste">Вставить</string>
|
||||||
<string name="common_read_more">Подробнее</string>
|
<string name="common_read_more">Подробнее</string>
|
||||||
<string name="common_receive">Получить</string>
|
<string name="common_receive">Получить</string>
|
||||||
<string name="common_reject">Отклонить</string>
|
<string name="common_reject">Отклонить</string>
|
||||||
<string name="common_reload">Перезагрузить</string>
|
<string name="common_reload">Перезагрузить</string>
|
||||||
<string name="common_rename">Переименовать</string>
|
<string name="common_rename">Переименовать</string>
|
||||||
|
<string name="common_retry">Повторить</string>
|
||||||
<string name="common_save_changes">Сохранить изменения</string>
|
<string name="common_save_changes">Сохранить изменения</string>
|
||||||
<string name="common_search">Искать</string>
|
<string name="common_search">Искать</string>
|
||||||
<string name="common_search_tokens">Поиск токенов</string>
|
<string name="common_search_tokens">Поиск токенов</string>
|
||||||
|
|
@ -125,6 +138,7 @@
|
||||||
<string name="common_understand">Я понял</string>
|
<string name="common_understand">Я понял</string>
|
||||||
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
<string name="common_unknown_error">Произошла ошибка. Пожалуйста, попробуйте снова.</string>
|
||||||
<string name="common_unreachable">Недоступно</string>
|
<string name="common_unreachable">Недоступно</string>
|
||||||
|
<string name="common_warning">Предупреждение</string>
|
||||||
<string name="common_yes">Да</string>
|
<string name="common_yes">Да</string>
|
||||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||||
<string name="currency_subtitle_expanded">Доступные сети</string>
|
<string name="currency_subtitle_expanded">Доступные сети</string>
|
||||||
|
|
@ -171,6 +185,7 @@
|
||||||
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
|
<string name="details_row_title_flip_to_hide">Скрывать балансы жестом переворота</string>
|
||||||
<string name="details_row_title_issuer">Эмитент</string>
|
<string name="details_row_title_issuer">Эмитент</string>
|
||||||
<string name="details_row_title_signed_hashes">Подписано</string>
|
<string name="details_row_title_signed_hashes">Подписано</string>
|
||||||
|
<string name="details_security_management_warning">Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно.</string>
|
||||||
<string name="details_title">Подробности</string>
|
<string name="details_title">Подробности</string>
|
||||||
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
|
<string name="disclaimer_error_loading">Проверьте подключение с интернетом или переключитесь на другую сеть</string>
|
||||||
<string name="disclaimer_title">Условия использования</string>
|
<string name="disclaimer_title">Условия использования</string>
|
||||||
|
|
@ -183,9 +198,9 @@
|
||||||
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
|
<string name="express_choose_providers_subtitle">Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами</string>
|
||||||
<string name="express_choose_providers_title">Выберите провайдера</string>
|
<string name="express_choose_providers_title">Выберите провайдера</string>
|
||||||
<string name="express_error_code">Произошла ошибка. Код: %s</string>
|
<string name="express_error_code">Произошла ошибка. Код: %s</string>
|
||||||
<string name="express_error_provider_unavailable">К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код: %s)</string>
|
<string name="express_error_provider_unavailable">К сожалению, обмен указанной пары через выбранного провайдера на данный момент невозможен. Попробуйте совершить обмен позже. (Код: %s)</string>
|
||||||
<string name="express_error_swap_pair_unavailable">Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s)</string>
|
<string name="express_error_swap_pair_unavailable">Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код: %s)</string>
|
||||||
<string name="express_error_swap_unavailable">В данный момент обмен не возможен. Попробуйте позже. (Код: %s)</string>
|
<string name="express_error_swap_unavailable">В данный момент обмен невозможен. Попробуйте позже. (Код: %s)</string>
|
||||||
<string name="express_estimated_amount">Курс обмена</string>
|
<string name="express_estimated_amount">Курс обмена</string>
|
||||||
<string name="express_exchange_by">Обмен через %s</string>
|
<string name="express_exchange_by">Обмен через %s</string>
|
||||||
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
|
<string name="express_exchange_notification_failed_text">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
|
||||||
|
|
@ -288,7 +303,7 @@
|
||||||
<string name="onboarding_activation_error_title">Ошибка активации</string>
|
<string name="onboarding_activation_error_title">Ошибка активации</string>
|
||||||
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
||||||
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
||||||
<string name="onboarding_bottom_sheet_passphrase_description">Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.</string>
|
<string name="onboarding_bottom_sheet_passphrase_description">Парольная фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов.</string>
|
||||||
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
||||||
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
|
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
|
||||||
<string name="onboarding_button_backup_now">Создать резервную копию</string>
|
<string name="onboarding_button_backup_now">Создать резервную копию</string>
|
||||||
|
|
@ -425,8 +440,6 @@
|
||||||
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
|
<string name="scan_card_settings_message">Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку.</string>
|
||||||
<string name="scan_card_settings_title">Приготовьте свою карту</string>
|
<string name="scan_card_settings_title">Приготовьте свою карту</string>
|
||||||
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
|
<string name="send_additional_field_already_included">Уже содержится в введенном адресе</string>
|
||||||
<string name="send_alert_fee_coverage_subract_text">Вычесть</string>
|
|
||||||
<string name="send_alert_fee_coverage_title">Недостаточно средств для покрытия комиссии сети. Вычесть недостающую сумму для покрытия комиссии из отправляемой суммы?</string>
|
|
||||||
<string name="send_alert_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
<string name="send_alert_fee_too_high_text">Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна.</string>
|
||||||
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
|
<string name="send_alert_fee_too_low_text">Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить?</string>
|
||||||
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
<string name="send_alert_transaction_failed_text">Причина: %1$s\nКод: %2$s</string>
|
||||||
|
|
@ -478,7 +491,7 @@
|
||||||
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
|
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
|
||||||
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
<string name="send_notification_transaction_limit_text">Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму.</string>
|
||||||
<string name="send_notification_transaction_limit_title">Лимит транзакции</string>
|
<string name="send_notification_transaction_limit_title">Лимит транзакции</string>
|
||||||
<string name="send_optional_field">Необязательное</string>
|
<string name="send_optional_field">Опционально</string>
|
||||||
<string name="send_qrcode_scan_info">Пожалуйста, совместите свой QR-код с квадратом, чтобы отсканировать его. Убедитесь, что вы сканируете адрес в сети %s.</string>
|
<string name="send_qrcode_scan_info">Пожалуйста, совместите свой QR-код с квадратом, чтобы отсканировать его. Убедитесь, что вы сканируете адрес в сети %s.</string>
|
||||||
<string name="send_recent_transactions">Последние</string>
|
<string name="send_recent_transactions">Последние</string>
|
||||||
<string name="send_recipient">Получатель</string>
|
<string name="send_recipient">Получатель</string>
|
||||||
|
|
@ -543,6 +556,7 @@
|
||||||
<string name="token_button_unavailability_reason_no_quotes">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
<string name="token_button_unavailability_reason_no_quotes">Выбранная операция в данный момент недоступна. Попробуйте позже.</string>
|
||||||
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %s не доступен. Но мы работаем над его добавлением.</string>
|
<string name="token_button_unavailability_reason_not_exchangeable">Обмен %s не доступен. Но мы работаем над его добавлением.</string>
|
||||||
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением.</string>
|
<string name="token_button_unavailability_reason_sell_unavailable">В данный момент продажа монеты %s недоступна. Но мы работаем над её добавлением.</string>
|
||||||
|
<string name="token_details_choose_address">Выберите адрес</string>
|
||||||
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
|
<string name="token_details_generate_xpub">Сгенерировать XPUB</string>
|
||||||
<string name="token_details_hide_alert_hide">Скрыть</string>
|
<string name="token_details_hide_alert_hide">Скрыть</string>
|
||||||
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
|
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
|
||||||
|
|
@ -652,6 +666,10 @@
|
||||||
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
|
<string name="warning_express_wrong_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||||
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
||||||
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_button_title">Ассоциировать</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_message">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%.4f %s</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_message_brief">Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_title">Ассоциируете свой токен</string>
|
||||||
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
||||||
<string name="warning_low_signatures_title">Малое количество подписей</string>
|
<string name="warning_low_signatures_title">Малое количество подписей</string>
|
||||||
<string name="warning_manage_tokens_legacy_derivation_message">Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.</string>
|
<string name="warning_manage_tokens_legacy_derivation_message">Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети.</string>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@
|
||||||
<string name="add_tokens_title">Manage tokens</string>
|
<string name="add_tokens_title">Manage tokens</string>
|
||||||
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||||
<string name="alert_button_request_support">Request support</string>
|
<string name="alert_button_request_support">Request support</string>
|
||||||
|
<string name="alert_button_try_again">Try again</string>
|
||||||
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
|
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
|
||||||
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
|
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
|
||||||
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
|
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
|
||||||
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
|
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
|
||||||
<string name="alert_manage_tokens_unsupported_curve_message">To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.</string>
|
<string name="alert_manage_tokens_unsupported_curve_message">To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.</string>
|
||||||
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
|
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
|
||||||
|
<string name="alert_negative_app_rate_sent_message">Thank you for your feedback. We will respond as soon as possible</string>
|
||||||
|
<string name="alert_negative_app_rate_sent_title">Your suggestions were sent</string>
|
||||||
|
<string name="alert_troubleshooting_scan_card_message">Please try to tap the card exactly as shown in the animation or request support.</string>
|
||||||
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
|
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
|
||||||
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
|
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
|
||||||
<string name="app_settings_default_fee">Default Fee</string>
|
<string name="app_settings_default_fee">Default Fee</string>
|
||||||
|
|
@ -25,6 +29,8 @@
|
||||||
<string name="app_settings_theme_mode_dark">Dark</string>
|
<string name="app_settings_theme_mode_dark">Dark</string>
|
||||||
<string name="app_settings_theme_mode_light">Light</string>
|
<string name="app_settings_theme_mode_light">Light</string>
|
||||||
<string name="app_settings_theme_mode_system">System default</string>
|
<string name="app_settings_theme_mode_system">System default</string>
|
||||||
|
<string name="app_settings_theme_selection_footer">If system is selected, the app will auto-adjust based on your device\'s system settings</string>
|
||||||
|
<string name="app_settings_theme_selection_system_short">System</string>
|
||||||
<string name="app_settings_theme_selector_title">Theme</string>
|
<string name="app_settings_theme_selector_title">Theme</string>
|
||||||
<string name="app_settings_title">App Settings</string>
|
<string name="app_settings_title">App Settings</string>
|
||||||
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
|
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
|
||||||
|
|
@ -50,6 +56,10 @@
|
||||||
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
|
<string name="card_settings_reset_card_to_factory">Reset to Factory Settings</string>
|
||||||
<string name="card_settings_security_mode">Security Mode</string>
|
<string name="card_settings_security_mode">Security Mode</string>
|
||||||
<string name="card_settings_title">Card Settings</string>
|
<string name="card_settings_title">Card Settings</string>
|
||||||
|
<string name="cardano_coin_will_be_send_with_token_description">Due to the peculiarities of the Cardano network, when transacting the %1$s token, in addition to the network commission, %2$s will be charged</string>
|
||||||
|
<string name="cardano_insufficient_balance_to_send_token_description">To make a %1$s transaction, you must deposit some %2$s (%3$s) to cover the network fee and minimum ADA value</string>
|
||||||
|
<string name="cardano_insufficient_balance_to_send_token_title">Insufficient ADA to token transfer</string>
|
||||||
|
<string name="cardano_max_amount_has_token_description">Withdrawal of the entire ADA balance is not possible if funds are available in Cardano network tokens. First, withdraw funds on your tokens.</string>
|
||||||
<string name="common_accept">Accept</string>
|
<string name="common_accept">Accept</string>
|
||||||
<string name="common_access_denied">Access denied</string>
|
<string name="common_access_denied">Access denied</string>
|
||||||
<string name="common_apply">Apply</string>
|
<string name="common_apply">Apply</string>
|
||||||
|
|
@ -70,6 +80,7 @@
|
||||||
<string name="common_create">Create</string>
|
<string name="common_create">Create</string>
|
||||||
<string name="common_delete">Delete</string>
|
<string name="common_delete">Delete</string>
|
||||||
<string name="common_disabled">Disabled</string>
|
<string name="common_disabled">Disabled</string>
|
||||||
|
<string name="common_disconnect">Disconnect</string>
|
||||||
<string name="common_done">Done</string>
|
<string name="common_done">Done</string>
|
||||||
<string name="common_enable">Enable</string>
|
<string name="common_enable">Enable</string>
|
||||||
<string name="common_enabled">Enabled</string>
|
<string name="common_enabled">Enabled</string>
|
||||||
|
|
@ -96,11 +107,13 @@
|
||||||
<string name="common_ok">OK</string>
|
<string name="common_ok">OK</string>
|
||||||
<string name="common_origin_card">Primary Card</string>
|
<string name="common_origin_card">Primary Card</string>
|
||||||
<string name="common_passphrase">Passphrase</string>
|
<string name="common_passphrase">Passphrase</string>
|
||||||
|
<string name="common_paste">Paste</string>
|
||||||
<string name="common_read_more">Read more</string>
|
<string name="common_read_more">Read more</string>
|
||||||
<string name="common_receive">Receive</string>
|
<string name="common_receive">Receive</string>
|
||||||
<string name="common_reject">Reject</string>
|
<string name="common_reject">Reject</string>
|
||||||
<string name="common_reload">Reload</string>
|
<string name="common_reload">Reload</string>
|
||||||
<string name="common_rename">Rename</string>
|
<string name="common_rename">Rename</string>
|
||||||
|
<string name="common_retry">Retry</string>
|
||||||
<string name="common_save_changes">Save changes</string>
|
<string name="common_save_changes">Save changes</string>
|
||||||
<string name="common_search">Search</string>
|
<string name="common_search">Search</string>
|
||||||
<string name="common_search_tokens">Search tokens</string>
|
<string name="common_search_tokens">Search tokens</string>
|
||||||
|
|
@ -124,6 +137,7 @@
|
||||||
<string name="common_understand">I understand</string>
|
<string name="common_understand">I understand</string>
|
||||||
<string name="common_unknown_error">There was an error. Please try again.</string>
|
<string name="common_unknown_error">There was an error. Please try again.</string>
|
||||||
<string name="common_unreachable">Unreachable</string>
|
<string name="common_unreachable">Unreachable</string>
|
||||||
|
<string name="common_warning">Warning</string>
|
||||||
<string name="common_yes">Yes</string>
|
<string name="common_yes">Yes</string>
|
||||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||||
<string name="currency_subtitle_expanded">Available networks</string>
|
<string name="currency_subtitle_expanded">Available networks</string>
|
||||||
|
|
@ -170,6 +184,7 @@
|
||||||
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
|
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
|
||||||
<string name="details_row_title_issuer">Issuer</string>
|
<string name="details_row_title_issuer">Issuer</string>
|
||||||
<string name="details_row_title_signed_hashes">Signed</string>
|
<string name="details_row_title_signed_hashes">Signed</string>
|
||||||
|
<string name="details_security_management_warning">If you forget the code you will lose access to your funds. Code recovery is not possible.</string>
|
||||||
<string name="details_title">Details</string>
|
<string name="details_title">Details</string>
|
||||||
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
|
<string name="disclaimer_error_loading">Check your internet connection or switch to a different network</string>
|
||||||
<string name="disclaimer_title">Terms of Service</string>
|
<string name="disclaimer_title">Terms of Service</string>
|
||||||
|
|
@ -420,13 +435,13 @@
|
||||||
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
|
<string name="scan_card_settings_message">Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.</string>
|
||||||
<string name="scan_card_settings_title">Get your card ready!</string>
|
<string name="scan_card_settings_title">Get your card ready!</string>
|
||||||
<string name="send_additional_field_already_included">Already included in the entered address</string>
|
<string name="send_additional_field_already_included">Already included in the entered address</string>
|
||||||
<string name="send_alert_fee_coverage_subract_text">Subtract</string>
|
|
||||||
<string name="send_alert_fee_coverage_title">Not enough funds to cover the network fee. Do you want to subtract the amount required to cover the fee?</string>
|
|
||||||
<string name="send_alert_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
<string name="send_alert_fee_too_high_text">The commission amount is %s times the recommended amount. Make sure that the custom settings are correct.</string>
|
||||||
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
|
<string name="send_alert_fee_too_low_text">You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue?</string>
|
||||||
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
<string name="send_alert_transaction_failed_text">Reason: %1$s\nCode: %2$s</string>
|
||||||
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
<string name="send_alert_transaction_failed_title">The transaction is not completed</string>
|
||||||
<string name="send_amount_label">Amount</string>
|
<string name="send_amount_label">Amount</string>
|
||||||
|
<string name="send_base_fee">Base fee</string>
|
||||||
|
<string name="send_base_fee_footer">Represents the part of the transaction fee that goes to the miner</string>
|
||||||
<string name="send_date_format">%1$s, %2$s</string>
|
<string name="send_date_format">%1$s, %2$s</string>
|
||||||
<string name="send_destination_hint_address">Address</string>
|
<string name="send_destination_hint_address">Address</string>
|
||||||
<string name="send_destination_tag_field">Destination Tag</string>
|
<string name="send_destination_tag_field">Destination Tag</string>
|
||||||
|
|
@ -474,6 +489,8 @@
|
||||||
<string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
|
<string name="send_notification_transaction_limit_text">Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount.</string>
|
||||||
<string name="send_notification_transaction_limit_title">Transaction limitation</string>
|
<string name="send_notification_transaction_limit_title">Transaction limitation</string>
|
||||||
<string name="send_optional_field">Optional</string>
|
<string name="send_optional_field">Optional</string>
|
||||||
|
<string name="send_priority_fee">Priority fee</string>
|
||||||
|
<string name="send_priority_fee_footer">Represents the minimum gasUsed multiplier required for a transaction to be included in a block. This is the part of the transaction fee that is burnt.</string>
|
||||||
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
|
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
|
||||||
<string name="send_recent_transactions">Recent</string>
|
<string name="send_recent_transactions">Recent</string>
|
||||||
<string name="send_recipient">Recipient</string>
|
<string name="send_recipient">Recipient</string>
|
||||||
|
|
@ -540,6 +557,7 @@
|
||||||
<string name="token_button_unavailability_reason_no_quotes">This operation is currently unavailable. Please try again later.</string>
|
<string name="token_button_unavailability_reason_no_quotes">This operation is currently unavailable. Please try again later.</string>
|
||||||
<string name="token_button_unavailability_reason_not_exchangeable">%s swap is not available. But we are working on adding it.</string>
|
<string name="token_button_unavailability_reason_not_exchangeable">%s swap is not available. But we are working on adding it.</string>
|
||||||
<string name="token_button_unavailability_reason_sell_unavailable">Sell of the %s coin is currently unavailable. But we are working on adding it.</string>
|
<string name="token_button_unavailability_reason_sell_unavailable">Sell of the %s coin is currently unavailable. But we are working on adding it.</string>
|
||||||
|
<string name="token_details_choose_address">Choose address</string>
|
||||||
<string name="token_details_generate_xpub">Generate XPUB</string>
|
<string name="token_details_generate_xpub">Generate XPUB</string>
|
||||||
<string name="token_details_hide_alert_hide">Hide</string>
|
<string name="token_details_hide_alert_hide">Hide</string>
|
||||||
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
|
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
|
||||||
|
|
@ -649,6 +667,10 @@
|
||||||
<string name="warning_express_wrong_amount_description">Please change the amount to swap</string>
|
<string name="warning_express_wrong_amount_description">Please change the amount to swap</string>
|
||||||
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
||||||
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_button_title">Associate</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_message">This token must be associated with your Hedera account before you can receive it. Association fee ~%.4f %s</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_message_brief">This token must be associated with your Hedera account before you can receive it</string>
|
||||||
|
<string name="warning_hedera_missing_token_association_title">Associate your token</string>
|
||||||
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
||||||
<string name="warning_low_signatures_title">Low signature count</string>
|
<string name="warning_low_signatures_title">Low signature count</string>
|
||||||
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
|
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,15 @@ inline fun <reified T> SegmentedButtons(
|
||||||
selectedColor: Color = TangemTheme.colors.background.action,
|
selectedColor: Color = TangemTheme.colors.background.action,
|
||||||
dividerColor: Color = TangemTheme.colors.stroke.primary,
|
dividerColor: Color = TangemTheme.colors.stroke.primary,
|
||||||
showIndication: Boolean = true,
|
showIndication: Boolean = true,
|
||||||
selectedItem: T? = null,
|
initialSelectedItem: T? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
crossinline buttonContent: @Composable (T) -> Unit,
|
crossinline buttonContent: @Composable (T) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (config.isEmpty() || config.size == 1) return
|
if (config.isEmpty() || config.size == 1) return
|
||||||
|
|
||||||
var selected by remember {
|
var selectedIndex by remember {
|
||||||
val selectedIndex = if (selectedItem == null) 0 else config.indexOf(selectedItem)
|
val index = if (initialSelectedItem == null) 0 else config.indexOf(initialSelectedItem)
|
||||||
mutableIntStateOf(selectedIndex)
|
mutableIntStateOf(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -69,7 +70,7 @@ inline fun <reified T> SegmentedButtons(
|
||||||
val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
|
val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0
|
||||||
|
|
||||||
val animateColor by animateColorAsState(
|
val animateColor by animateColorAsState(
|
||||||
targetValue = if (index == selected) selectedColor else color,
|
targetValue = if (index == selectedIndex) selectedColor else color,
|
||||||
label = "Segmented Button Selected Color Animation",
|
label = "Segmented Button Selected Color Animation",
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessMedium),
|
animationSpec = spring(stiffness = Spring.StiffnessMedium),
|
||||||
)
|
)
|
||||||
|
|
@ -86,11 +87,12 @@ inline fun <reified T> SegmentedButtons(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
|
enabled = isEnabled,
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
indication = if (showIndication) LocalIndication.current else null,
|
indication = if (showIndication) LocalIndication.current else null,
|
||||||
) {
|
) {
|
||||||
|
selectedIndex = index
|
||||||
onClick(config[index])
|
onClick(config[index])
|
||||||
selected = index
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
buttonContent.invoke(config[index])
|
buttonContent.invoke(config[index])
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.ColorMatrix
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||||
|
|
||||||
|
private const val GRAY_SCALE_SATURATION = 0f
|
||||||
|
private const val GRAY_SCALE_ALPHA = 0.4f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple icon from network
|
* Simple icon from network
|
||||||
*
|
*
|
||||||
|
|
@ -20,16 +25,19 @@ import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
|
||||||
fun FiatIcon(
|
fun FiatIcon(
|
||||||
url: String?,
|
url: String?,
|
||||||
size: Dp,
|
size: Dp,
|
||||||
|
isGrayscale: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
|
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
|
||||||
) {
|
) {
|
||||||
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
|
||||||
|
val alpha = if (isGrayscale) GRAY_SCALE_ALPHA else 1f
|
||||||
|
val colorFilter = if (isGrayscale) GrayscaleColorFilter else null
|
||||||
|
|
||||||
DefaultCurrencyIcon(
|
DefaultCurrencyIcon(
|
||||||
iconData = iconData,
|
iconData = iconData,
|
||||||
size = size,
|
size = size,
|
||||||
alpha = 1f,
|
alpha = alpha,
|
||||||
colorFilter = null,
|
colorFilter = colorFilter,
|
||||||
errorIcon = {
|
errorIcon = {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = fallbackResId),
|
painter = painterResource(id = fallbackResId),
|
||||||
|
|
@ -38,4 +46,7 @@ fun FiatIcon(
|
||||||
},
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val GrayscaleColorFilter: ColorFilter
|
||||||
|
get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) })
|
||||||
|
|
@ -20,6 +20,26 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun convertCustom(
|
||||||
|
value: CryptoCurrencyStatus,
|
||||||
|
forceGrayscale: Boolean,
|
||||||
|
showCustomTokenBadge: Boolean,
|
||||||
|
): TokenIconState {
|
||||||
|
return when (val currency = value.currency) {
|
||||||
|
is CryptoCurrency.Coin -> getIconStateForCoin(
|
||||||
|
coin = currency,
|
||||||
|
isUnreachable = value.value.isError,
|
||||||
|
forceGrayscale = forceGrayscale,
|
||||||
|
)
|
||||||
|
is CryptoCurrency.Token -> getIconStateForToken(
|
||||||
|
token = currency,
|
||||||
|
isErrorStatus = value.value.isError,
|
||||||
|
forceGrayscale = forceGrayscale,
|
||||||
|
showCustomBadge = showCustomTokenBadge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun convert(currency: CryptoCurrency): TokenIconState {
|
fun convert(currency: CryptoCurrency): TokenIconState {
|
||||||
return when (currency) {
|
return when (currency) {
|
||||||
is CryptoCurrency.Coin -> getIconStateForCoin(currency, isUnreachable = false)
|
is CryptoCurrency.Coin -> getIconStateForCoin(currency, isUnreachable = false)
|
||||||
|
|
@ -27,18 +47,27 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon {
|
private fun getIconStateForCoin(
|
||||||
|
coin: CryptoCurrency.Coin,
|
||||||
|
isUnreachable: Boolean,
|
||||||
|
forceGrayscale: Boolean = false,
|
||||||
|
): TokenIconState.CoinIcon {
|
||||||
return TokenIconState.CoinIcon(
|
return TokenIconState.CoinIcon(
|
||||||
url = coin.iconUrl,
|
url = coin.iconUrl,
|
||||||
fallbackResId = coin.networkIconResId,
|
fallbackResId = coin.networkIconResId,
|
||||||
isGrayscale = coin.network.isTestnet || isUnreachable,
|
isGrayscale = forceGrayscale || coin.network.isTestnet || isUnreachable,
|
||||||
showCustomBadge = coin.isCustom,
|
showCustomBadge = coin.isCustom,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenIconState {
|
private fun getIconStateForToken(
|
||||||
val isGrayscale = token.network.isTestnet || isErrorStatus
|
token: CryptoCurrency.Token,
|
||||||
val background = token.tryGetBackgroundForTokenIcon(isGrayscale)
|
isErrorStatus: Boolean,
|
||||||
|
showCustomBadge: Boolean = true,
|
||||||
|
forceGrayscale: Boolean = false,
|
||||||
|
): TokenIconState {
|
||||||
|
val grayScale = forceGrayscale || token.network.isTestnet || isErrorStatus
|
||||||
|
val background = token.tryGetBackgroundForTokenIcon(grayScale)
|
||||||
val tint = getTintForTokenIcon(background)
|
val tint = getTintForTokenIcon(background)
|
||||||
|
|
||||||
return if (token.isCustom && token.iconUrl == null) {
|
return if (token.isCustom && token.iconUrl == null) {
|
||||||
|
|
@ -46,16 +75,16 @@ class CryptoCurrencyToIconStateConverter : Converter<CryptoCurrencyStatus, Token
|
||||||
tint = tint,
|
tint = tint,
|
||||||
background = background,
|
background = background,
|
||||||
networkBadgeIconResId = token.networkIconResId,
|
networkBadgeIconResId = token.networkIconResId,
|
||||||
isGrayscale = isGrayscale,
|
isGrayscale = grayScale,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
TokenIconState.TokenIcon(
|
TokenIconState.TokenIcon(
|
||||||
url = token.iconUrl,
|
url = token.iconUrl,
|
||||||
networkBadgeIconResId = token.networkIconResId,
|
networkBadgeIconResId = token.networkIconResId,
|
||||||
isGrayscale = isGrayscale,
|
isGrayscale = grayScale,
|
||||||
fallbackTint = tint,
|
fallbackTint = tint,
|
||||||
fallbackBackground = background,
|
fallbackBackground = background,
|
||||||
showCustomBadge = token.isCustom, // `true` for tokens with custom derivation
|
showCustomBadge = token.isCustom && showCustomBadge, // `true` for tokens with custom derivation
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Paste",
|
text = stringResource(R.string.common_paste),
|
||||||
style = TangemTheme.typography.button,
|
style = TangemTheme.typography.button,
|
||||||
color = TangemTheme.colors.text.primary2,
|
color = TangemTheme.colors.text.primary2,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.utils.toDateFormat
|
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||||
import com.tangem.core.ui.utils.toTimeFormat
|
import com.tangem.core.ui.utils.toTimeFormat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,7 +47,11 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie
|
||||||
.padding(top = TangemTheme.dimens.spacing16),
|
.padding(top = TangemTheme.dimens.spacing16),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),
|
text = stringResource(
|
||||||
|
id = R.string.send_date_format,
|
||||||
|
date.toTimeFormat(DateTimeFormatters.dateFormatter),
|
||||||
|
date.toTimeFormat(),
|
||||||
|
),
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
color = TangemTheme.colors.text.tertiary,
|
color = TangemTheme.colors.text.tertiary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,29 @@ object BigDecimalFormatter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun formatCryptoAmountUncapped(
|
||||||
|
cryptoAmount: BigDecimal?,
|
||||||
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
locale: Locale = Locale.getDefault(),
|
||||||
|
): String {
|
||||||
|
if (cryptoAmount == null) return EMPTY_BALANCE_SIGN
|
||||||
|
|
||||||
|
val formatter = NumberFormat.getNumberInstance(locale).apply {
|
||||||
|
maximumFractionDigits = cryptoCurrency.decimals
|
||||||
|
minimumFractionDigits = 2
|
||||||
|
isGroupingUsed = true
|
||||||
|
roundingMode = RoundingMode.DOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatter.format(cryptoAmount).let {
|
||||||
|
if (cryptoCurrency.symbol.isEmpty()) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it + "\u2009${cryptoCurrency.symbol}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun formatCryptoAmount(
|
fun formatCryptoAmount(
|
||||||
cryptoAmount: BigDecimal?,
|
cryptoAmount: BigDecimal?,
|
||||||
cryptoCurrency: CryptoCurrency,
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import org.joda.time.format.DateTimeFormatter
|
||||||
* If [this] timestamp is today or yesterday, returns relative date,
|
* If [this] timestamp is today or yesterday, returns relative date,
|
||||||
* otherwise returns formatting date.
|
* otherwise returns formatting date.
|
||||||
*/
|
*/
|
||||||
fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
|
fun Long.toDateFormatWithTodayYesterday(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
|
||||||
val localDate = DateTime(this, DateTimeZone.getDefault())
|
val localDate = DateTime(this, DateTimeZone.getDefault())
|
||||||
return if (localDate.isToday() || localDate.isYesterday()) {
|
return if (localDate.isToday() || localDate.isYesterday()) {
|
||||||
DateUtils.getRelativeTimeSpanString(
|
DateUtils.getRelativeTimeSpanString(
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ import com.tangem.domain.qrscanning.models.QrResult
|
||||||
import com.tangem.domain.qrscanning.models.SourceType
|
import com.tangem.domain.qrscanning.models.SourceType
|
||||||
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.yield
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
|
|
@ -16,15 +16,20 @@ internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository {
|
||||||
|
|
||||||
private data class QrScanningEvent(val type: SourceType, val qrCode: String)
|
private data class QrScanningEvent(val type: SourceType, val qrCode: String)
|
||||||
|
|
||||||
private val scannedEvents = MutableSharedFlow<QrScanningEvent>()
|
private val scannedEvents = MutableSharedFlow<QrScanningEvent>(replay = 1)
|
||||||
|
|
||||||
override suspend fun emitResult(type: SourceType, qrCode: String) {
|
override suspend fun emitResult(type: SourceType, qrCode: String) {
|
||||||
scannedEvents.emit(QrScanningEvent(type, qrCode))
|
scannedEvents.emit(QrScanningEvent(type, qrCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun subscribeToScanningResults(type: SourceType) = scannedEvents
|
override fun subscribeToScanningResults(type: SourceType) = scannedEvents
|
||||||
.filter { it.type == type }
|
.filter { it.type == type }
|
||||||
.map { it.qrCode }
|
.map { it.qrCode }
|
||||||
|
.onEach {
|
||||||
|
yield() // if we have more than one sub, we must allow them to collect emitted value
|
||||||
|
scannedEvents.resetReplayCache()
|
||||||
|
}
|
||||||
|
|
||||||
override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult {
|
override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult {
|
||||||
val withoutSchema = stripSchema(qrCode, cryptoCurrency)
|
val withoutSchema = stripSchema(qrCode, cryptoCurrency)
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(deps.tangem.blockchain)
|
implementation(deps.tangem.blockchain)
|
||||||
|
implementation(deps.tangem.card.core)
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.tap.features.demo
|
package com.tangem.domain.demo
|
||||||
|
|
||||||
import com.tangem.blockchain.common.*
|
import com.tangem.blockchain.common.*
|
||||||
import com.tangem.blockchain.common.transaction.Fee
|
import com.tangem.blockchain.common.transaction.Fee
|
||||||
|
|
@ -184,8 +184,9 @@ sealed class CryptoCurrency : Parcelable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
// should use delimiters that could be used in URL not like path or query delimiters
|
||||||
const val PREFIX_DELIMITER = '_'
|
const val PREFIX_DELIMITER = '_'
|
||||||
const val SUFFIX_DELIMITER = '#'
|
const val SUFFIX_DELIMITER = ';'
|
||||||
const val DERIVATION_PATH_DELIMITER = 'd'
|
const val DERIVATION_PATH_DELIMITER = 'd'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
|
||||||
|
|
||||||
sealed class SendTransactionError {
|
sealed class SendTransactionError {
|
||||||
|
|
||||||
object DemoCardError : SendTransactionError()
|
data object DemoCardError : SendTransactionError()
|
||||||
|
|
||||||
data class DataError(val message: String?) : SendTransactionError()
|
data class DataError(val message: String?) : SendTransactionError()
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ sealed class SendTransactionError {
|
||||||
|
|
||||||
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
data class BlockchainSdkError(val code: Int, val message: String?) : SendTransactionError()
|
||||||
|
|
||||||
object UserCancelledError : SendTransactionError()
|
data object UserCancelledError : SendTransactionError()
|
||||||
|
|
||||||
data class CreateAccountUnderfunded(val amount: String) : SendTransactionError()
|
data class CreateAccountUnderfunded(val amount: String) : SendTransactionError()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import com.tangem.blockchain.common.Amount
|
||||||
import com.tangem.blockchain.common.AmountType
|
import com.tangem.blockchain.common.AmountType
|
||||||
import com.tangem.blockchain.common.Token
|
import com.tangem.blockchain.common.Token
|
||||||
import com.tangem.blockchain.extensions.Result
|
import com.tangem.blockchain.extensions.Result
|
||||||
|
import com.tangem.domain.demo.DemoConfig
|
||||||
|
import com.tangem.domain.demo.DemoTransactionSender
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.transaction.error.GetFeeError
|
import com.tangem.domain.transaction.error.GetFeeError
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -17,23 +19,31 @@ import java.math.BigDecimal
|
||||||
*/
|
*/
|
||||||
class GetFeeUseCase(
|
class GetFeeUseCase(
|
||||||
private val walletManagersFacade: WalletManagersFacade,
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
|
private val demoConfig: DemoConfig,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
amount: BigDecimal,
|
amount: BigDecimal,
|
||||||
destination: String,
|
destination: String,
|
||||||
userWalletId: UserWalletId,
|
userWallet: UserWallet,
|
||||||
cryptoCurrency: CryptoCurrency,
|
cryptoCurrency: CryptoCurrency,
|
||||||
) = either {
|
) = either {
|
||||||
catch(
|
catch(
|
||||||
block = {
|
block = {
|
||||||
val result = requireNotNull(
|
val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount)
|
||||||
walletManagersFacade.getFee(
|
|
||||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) {
|
||||||
|
demoTransactionSender(userWallet, cryptoCurrency).getFee(
|
||||||
|
amount = amountData,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
userWalletId = userWalletId,
|
)
|
||||||
|
} else {
|
||||||
|
walletManagersFacade.getFee(
|
||||||
|
amount = amountData,
|
||||||
|
destination = destination,
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
network = cryptoCurrency.network,
|
network = cryptoCurrency.network,
|
||||||
),
|
) ?: error("Fee is null")
|
||||||
) { "Fee is null" }
|
}
|
||||||
|
|
||||||
val maybeFee = when (result) {
|
val maybeFee = when (result) {
|
||||||
is Result.Success -> result.data
|
is Result.Success -> result.data
|
||||||
|
|
@ -47,6 +57,17 @@ class GetFeeUseCase(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun demoTransactionSender(
|
||||||
|
userWallet: UserWallet,
|
||||||
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
): DemoTransactionSender {
|
||||||
|
return DemoTransactionSender(
|
||||||
|
walletManagersFacade
|
||||||
|
.getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network)
|
||||||
|
?: error("WalletManager is null"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||||
currencySymbol = cryptoCurrency.symbol,
|
currencySymbol = cryptoCurrency.symbol,
|
||||||
value = amount,
|
value = amount,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import com.tangem.blockchain.common.BlockchainSdkError
|
import com.tangem.blockchain.common.BlockchainSdkError
|
||||||
import com.tangem.blockchain.common.TransactionData
|
import com.tangem.blockchain.common.TransactionData
|
||||||
|
import com.tangem.blockchain.common.TransactionSigner
|
||||||
import com.tangem.blockchain.extensions.SimpleResult
|
import com.tangem.blockchain.extensions.SimpleResult
|
||||||
import com.tangem.blockchain.network.ResultChecker
|
import com.tangem.blockchain.network.ResultChecker
|
||||||
import com.tangem.common.core.TangemSdkError
|
import com.tangem.common.core.TangemSdkError
|
||||||
|
|
@ -12,20 +13,23 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.DemoConfig
|
||||||
|
import com.tangem.domain.demo.DemoTransactionSender
|
||||||
import com.tangem.domain.tokens.model.Network
|
import com.tangem.domain.tokens.model.Network
|
||||||
import com.tangem.domain.transaction.R
|
import com.tangem.domain.transaction.R
|
||||||
import com.tangem.domain.transaction.TransactionRepository
|
import com.tangem.domain.transaction.TransactionRepository
|
||||||
import com.tangem.domain.transaction.error.SendTransactionError
|
import com.tangem.domain.transaction.error.SendTransactionError
|
||||||
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_CANCELLED_ERROR_CODE
|
||||||
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||||
import com.tangem.utils.toFormattedString
|
import com.tangem.utils.toFormattedString
|
||||||
|
|
||||||
class SendTransactionUseCase(
|
class SendTransactionUseCase(
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val demoConfig: DemoConfig,
|
||||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
private val transactionRepository: TransactionRepository,
|
private val transactionRepository: TransactionRepository,
|
||||||
|
private val walletManagersFacade: WalletManagersFacade,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
txData: TransactionData,
|
txData: TransactionData,
|
||||||
|
|
@ -39,8 +43,13 @@ class SendTransactionUseCase(
|
||||||
cardSdkConfigRepository.setLinkedTerminal(false)
|
cardSdkConfigRepository.setLinkedTerminal(false)
|
||||||
}
|
}
|
||||||
val sendResult = try {
|
val sendResult = try {
|
||||||
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
|
if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) {
|
||||||
SendTransactionError.DemoCardError.left()
|
sendDemo(
|
||||||
|
userWallet = userWallet,
|
||||||
|
network = network,
|
||||||
|
transactionData = txData,
|
||||||
|
signer = signer,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
transactionRepository.sendTransaction(
|
transactionRepository.sendTransaction(
|
||||||
txData = txData,
|
txData = txData,
|
||||||
|
|
@ -66,6 +75,27 @@ class SendTransactionUseCase(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun sendDemo(
|
||||||
|
userWallet: UserWallet,
|
||||||
|
network: Network,
|
||||||
|
transactionData: TransactionData,
|
||||||
|
signer: TransactionSigner,
|
||||||
|
): Either<SendTransactionError, SimpleResult> {
|
||||||
|
val demoTransactionSender = DemoTransactionSender(
|
||||||
|
walletManagersFacade
|
||||||
|
.getOrCreateWalletManager(userWallet.walletId, network)
|
||||||
|
?: error("WalletManager is null"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = demoTransactionSender.send(transactionData = transactionData, signer = signer)
|
||||||
|
|
||||||
|
return if (result is SimpleResult.Failure && result.error.customMessage.contains(DemoTransactionSender.ID)) {
|
||||||
|
SendTransactionError.DemoCardError.left()
|
||||||
|
} else {
|
||||||
|
result.right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
private fun handleError(result: SimpleResult.Failure): SendTransactionError {
|
||||||
if (ResultChecker.isNetworkError(result)) {
|
if (ResultChecker.isNetworkError(result)) {
|
||||||
return SendTransactionError.NetworkError(
|
return SendTransactionError.NetworkError(
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modif
|
||||||
SegmentedButtons(
|
SegmentedButtons(
|
||||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
|
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76),
|
||||||
config = state.seedSegments,
|
config = state.seedSegments,
|
||||||
selectedItem = state.selectedSeedType,
|
initialSelectedItem = state.selectedSeedType,
|
||||||
onClick = {
|
onClick = {
|
||||||
state.onSelectType(it)
|
state.onSelectType(it)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.Manifest
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
@ -15,6 +16,8 @@ import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
|
@ -25,6 +28,8 @@ import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||||
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
import com.tangem.feature.qrscanning.presentation.QrScanningContent
|
||||||
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
|
import com.tangem.feature.qrscanning.viewmodel.QrScanningViewModel
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -69,11 +74,24 @@ internal class QrScanningFragment : ComposeFragment() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
viewModel.setRouter(innerRouter, galleryLauncher)
|
viewModel.setRouter(innerRouter)
|
||||||
cameraExecutor = Executors.newSingleThreadExecutor()
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
||||||
requestCameraPermission()
|
requestCameraPermission()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
viewModel.launchGalleryEvent
|
||||||
|
.collect {
|
||||||
|
galleryLauncher.launch(it.imageFilter)
|
||||||
|
delay(timeMillis = 2000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
checkPermissionGranted()
|
checkPermissionGranted()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.feature.qrscanning.viewmodel
|
package com.tangem.feature.qrscanning.viewmodel
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import com.tangem.domain.qrscanning.models.SourceType
|
import com.tangem.domain.qrscanning.models.SourceType
|
||||||
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -11,23 +10,14 @@ internal open class BaseQrScanningClickIntents {
|
||||||
protected val router: QrScanningInnerRouter get() = _router
|
protected val router: QrScanningInnerRouter get() = _router
|
||||||
protected val viewModelScope: CoroutineScope get() = _viewModelScope
|
protected val viewModelScope: CoroutineScope get() = _viewModelScope
|
||||||
protected val source: SourceType get() = _source
|
protected val source: SourceType get() = _source
|
||||||
protected val galleryLauncher: ActivityResultLauncher<String> get() = _galleryLauncher
|
|
||||||
|
|
||||||
private var _router: QrScanningInnerRouter by Delegates.notNull()
|
private var _router: QrScanningInnerRouter by Delegates.notNull()
|
||||||
private var _viewModelScope: CoroutineScope by Delegates.notNull()
|
private var _viewModelScope: CoroutineScope by Delegates.notNull()
|
||||||
private var _source: SourceType by Delegates.notNull()
|
private var _source: SourceType by Delegates.notNull()
|
||||||
|
|
||||||
private var _galleryLauncher: ActivityResultLauncher<String> by Delegates.notNull()
|
open fun initialize(router: QrScanningInnerRouter, source: SourceType, coroutineScope: CoroutineScope) {
|
||||||
|
|
||||||
open fun initialize(
|
|
||||||
router: QrScanningInnerRouter,
|
|
||||||
source: SourceType,
|
|
||||||
galleryLauncher: ActivityResultLauncher<String>,
|
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
) {
|
|
||||||
_router = router
|
_router = router
|
||||||
_viewModelScope = coroutineScope
|
_viewModelScope = coroutineScope
|
||||||
_source = source
|
_source = source
|
||||||
_galleryLauncher = galleryLauncher
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,11 +5,16 @@ import com.tangem.feature.qrscanning.presentation.QrScanningStateController
|
||||||
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
|
import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface QrScanningClickIntents {
|
interface QrScanningClickIntents {
|
||||||
|
|
||||||
|
val launchGallery: SharedFlow<GalleryRequest>
|
||||||
|
|
||||||
fun onBackClick()
|
fun onBackClick()
|
||||||
|
|
||||||
fun onQrScanned(qrCode: String)
|
fun onQrScanned(qrCode: String)
|
||||||
|
|
@ -17,6 +22,11 @@ interface QrScanningClickIntents {
|
||||||
fun onGalleryClicked()
|
fun onGalleryClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmInline
|
||||||
|
value class GalleryRequest(
|
||||||
|
val imageFilter: String,
|
||||||
|
)
|
||||||
|
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
internal class QrScanningClickIntentsImplementor @Inject constructor(
|
internal class QrScanningClickIntentsImplementor @Inject constructor(
|
||||||
private val stateHolder: QrScanningStateController,
|
private val stateHolder: QrScanningStateController,
|
||||||
|
|
@ -26,6 +36,11 @@ internal class QrScanningClickIntentsImplementor @Inject constructor(
|
||||||
|
|
||||||
private var isScanned = false
|
private var isScanned = false
|
||||||
|
|
||||||
|
override val launchGallery = MutableSharedFlow<GalleryRequest>(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_LATEST,
|
||||||
|
)
|
||||||
|
|
||||||
override fun onBackClick() = router.popBackStack()
|
override fun onBackClick() = router.popBackStack()
|
||||||
|
|
||||||
override fun onQrScanned(qrCode: String) {
|
override fun onQrScanned(qrCode: String) {
|
||||||
|
|
@ -41,7 +56,7 @@ internal class QrScanningClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGalleryClicked() {
|
override fun onGalleryClicked() {
|
||||||
galleryLauncher.launch(GALLERY_IMAGE_FILTER)
|
launchGallery.tryEmit(GalleryRequest(imageFilter = GALLERY_IMAGE_FILTER))
|
||||||
if (stateHolder.value.bottomSheetConfig != null) {
|
if (stateHolder.value.bottomSheetConfig != null) {
|
||||||
stateHolder.update(DismissBottomSheetTransformer())
|
stateHolder.update(DismissBottomSheetTransformer())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.feature.qrscanning.viewmodel
|
package com.tangem.feature.qrscanning.viewmodel
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -14,6 +13,7 @@ import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomShee
|
||||||
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
|
import com.tangem.feature.qrscanning.presentation.transformers.InitializeQrScanningStateTransformer
|
||||||
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
|
import com.tangem.feature.qrscanning.presentation.transformers.ShowCameraDeniedBottomSheetTransformer
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -28,12 +28,12 @@ internal class QrScanningViewModel @Inject constructor(
|
||||||
private val network: String? = savedStateHandle[NETWORK_KEY]
|
private val network: String? = savedStateHandle[NETWORK_KEY]
|
||||||
|
|
||||||
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
|
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
|
||||||
|
val launchGalleryEvent: SharedFlow<GalleryRequest> = clickIntents.launchGallery
|
||||||
|
|
||||||
fun setRouter(router: QrScanningInnerRouter, galleryLauncher: ActivityResultLauncher<String>) {
|
fun setRouter(router: QrScanningInnerRouter) {
|
||||||
clickIntents.initialize(
|
clickIntents.initialize(
|
||||||
router = router,
|
router = router,
|
||||||
source = source,
|
source = source,
|
||||||
galleryLauncher = galleryLauncher,
|
|
||||||
coroutineScope = viewModelScope,
|
coroutineScope = viewModelScope,
|
||||||
)
|
)
|
||||||
stateHolder.update(InitializeQrScanningStateTransformer(clickIntents, source, network))
|
stateHolder.update(InitializeQrScanningStateTransformer(clickIntents, source, network))
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ import com.tangem.core.ui.components.SpacerH16
|
||||||
import com.tangem.core.ui.components.SpacerH24
|
import com.tangem.core.ui.components.SpacerH24
|
||||||
import com.tangem.core.ui.components.SpacerH32
|
import com.tangem.core.ui.components.SpacerH32
|
||||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||||
import com.tangem.core.ui.res.TangemColorPalette
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.feature.referral.domain.models.ExpectedAward
|
import com.tangem.feature.referral.domain.models.ExpectedAward
|
||||||
import com.tangem.feature.referral.domain.models.ExpectedAwards
|
import com.tangem.feature.referral.domain.models.ExpectedAwards
|
||||||
|
|
@ -350,7 +349,7 @@ private fun BoxScope.ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) {
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
actionOnNewLine = true,
|
actionOnNewLine = true,
|
||||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius8),
|
shape = RoundedCornerShape(size = TangemTheme.dimens.radius8),
|
||||||
containerColor = TangemColorPalette.Black,
|
containerColor = TangemTheme.colors.button.primary,
|
||||||
contentColor = TangemTheme.colors.text.primary2,
|
contentColor = TangemTheme.colors.text.primary2,
|
||||||
actionColor = TangemTheme.colors.text.primary2,
|
actionColor = TangemTheme.colors.text.primary2,
|
||||||
)
|
)
|
||||||
|
|
@ -464,7 +463,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
|
||||||
url = "",
|
url = "",
|
||||||
expectedAwards = null,
|
expectedAwards = null,
|
||||||
),
|
),
|
||||||
errorSnackbar = null,
|
errorSnackbar = ErrorSnackbar(DemoModeException()) {},
|
||||||
analytics = Analytics(
|
analytics = Analytics(
|
||||||
onAgreementClicked = {},
|
onAgreementClicked = {},
|
||||||
onCopyClicked = {},
|
onCopyClicked = {},
|
||||||
|
|
@ -493,7 +492,7 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() {
|
||||||
url = "",
|
url = "",
|
||||||
expectedAwards = null,
|
expectedAwards = null,
|
||||||
),
|
),
|
||||||
errorSnackbar = null,
|
errorSnackbar = ErrorSnackbar(DemoModeException()) {},
|
||||||
analytics = Analytics(
|
analytics = Analytics(
|
||||||
onAgreementClicked = {},
|
onAgreementClicked = {},
|
||||||
onCopyClicked = {},
|
onCopyClicked = {},
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,10 @@ internal sealed class SendAnalyticEvents(
|
||||||
params = mapOf(TOKEN to token),
|
params = mapOf(TOKEN to token),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data object NoticeFeeCoverage : SendAnalyticEvents(
|
||||||
|
event = "Notice - Network Fee Coverage",
|
||||||
|
)
|
||||||
|
|
||||||
/** If error occurs during send transactions */
|
/** If error occurs during send transactions */
|
||||||
data class TransactionError(val token: String) : SendAnalyticEvents(
|
data class TransactionError(val token: String) : SendAnalyticEvents(
|
||||||
event = "Error - Transaction Rejected",
|
event = "Error - Transaction Rejected",
|
||||||
|
|
|
||||||
|
|
@ -68,16 +68,6 @@ internal sealed class SendAlertState {
|
||||||
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
override val confirmButtonText: TextReference = resourceReference(R.string.common_continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeeCoverage(
|
|
||||||
override val onConfirmClick: (() -> Unit),
|
|
||||||
) : SendAlertState() {
|
|
||||||
override val title: TextReference? = null
|
|
||||||
override val message: TextReference =
|
|
||||||
resourceReference(id = R.string.send_alert_fee_coverage_title)
|
|
||||||
override val confirmButtonText: TextReference =
|
|
||||||
resourceReference(id = R.string.send_alert_fee_coverage_subract_text)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ReserveAmount(val amount: String) : SendAlertState() {
|
data class ReserveAmount(val amount: String) : SendAlertState() {
|
||||||
override val title: TextReference =
|
override val title: TextReference =
|
||||||
resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount))
|
resourceReference(id = R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount))
|
||||||
|
|
|
||||||
|
|
@ -48,19 +48,6 @@ internal class SendEventStateFactory(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFeeCoverageAlert(onConsume: () -> Unit): SendUiState {
|
|
||||||
return currentStateProvider().copy(
|
|
||||||
event = triggeredEvent(
|
|
||||||
data = SendEvent.ShowAlert(
|
|
||||||
SendAlertState.FeeCoverage(
|
|
||||||
onConfirmClick = clickIntents::onSubtractSelect,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onConsume = onConsume,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
|
fun getFeeUpdatedAlert(fee: TransactionFee, onConsume: () -> Unit, onFeeNotIncreased: () -> Unit): SendUiState {
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
|
val feeSelector = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return state
|
||||||
|
|
@ -139,9 +126,7 @@ internal class SendEventStateFactory(
|
||||||
return state.copy(
|
return state.copy(
|
||||||
event = triggeredEvent(
|
event = triggeredEvent(
|
||||||
data = SendEvent.ShowAlert(
|
data = SendEvent.ShowAlert(
|
||||||
SendAlertState.FeeUnreachableError(
|
SendAlertState.FeeUnreachableError(onConfirmClick = clickIntents::feeReload),
|
||||||
onConfirmClick = { clickIntents.feeReload(true) },
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
onConsume = onConsume,
|
onConsume = onConsume,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,6 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ReserveAmountError(val amount: String) : Error(
|
|
||||||
title = resourceReference(R.string.send_notification_invalid_reserve_amount_title, wrappedList(amount)),
|
|
||||||
subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text),
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TransactionLimitError(
|
data class TransactionLimitError(
|
||||||
val cryptoCurrency: String,
|
val cryptoCurrency: String,
|
||||||
val utxoLimit: String,
|
val utxoLimit: String,
|
||||||
|
|
@ -153,5 +148,10 @@ internal sealed class SendNotification(val config: NotificationConfig) {
|
||||||
onClick = onRefresh,
|
onClick = onRefresh,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data object FeeCoverageNotification : Warning(
|
||||||
|
title = resourceReference(R.string.send_network_fee_warning_title),
|
||||||
|
subtitle = resourceReference(R.string.swapping_network_fee_warning_content),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,6 @@ import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase
|
||||||
import com.tangem.features.send.impl.R
|
import com.tangem.features.send.impl.R
|
||||||
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
import com.tangem.features.send.impl.presentation.domain.AvailableWallet
|
||||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
|
||||||
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter
|
|
||||||
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
|
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
|
||||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
|
||||||
|
|
@ -48,12 +47,6 @@ internal class SendStateFactory(
|
||||||
appCurrencyProvider = appCurrencyProvider,
|
appCurrencyProvider = appCurrencyProvider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
private val amountSubtractConverter by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
SendAmountSubtractConverter(
|
|
||||||
currentStateProvider = currentStateProvider,
|
|
||||||
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
SendAmountStateConverter(
|
SendAmountStateConverter(
|
||||||
appCurrencyProvider = appCurrencyProvider,
|
appCurrencyProvider = appCurrencyProvider,
|
||||||
|
|
@ -117,6 +110,7 @@ internal class SendStateFactory(
|
||||||
recipientState = state.recipientState
|
recipientState = state.recipientState
|
||||||
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
|
||||||
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
feeState = state.feeState ?: feeStateConverter.convert(Unit),
|
||||||
|
sendState = confirmStateConverter.convert(Unit),
|
||||||
isEditingDisabled = true,
|
isEditingDisabled = true,
|
||||||
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
cryptoCurrencyName = cryptoCurrencyStatusProvider().currency.name,
|
||||||
)
|
)
|
||||||
|
|
@ -264,14 +258,6 @@ internal class SendStateFactory(
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region send
|
//region send
|
||||||
fun onSubtractSelect(isAmountSubtractAvailable: Boolean): SendUiState {
|
|
||||||
val state = currentStateProvider()
|
|
||||||
|
|
||||||
if (!isAmountSubtractAvailable) return state
|
|
||||||
|
|
||||||
return amountSubtractConverter.convert(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
fun getSendingStateUpdate(isSending: Boolean): SendUiState {
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
return state.copy(
|
return state.copy(
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,9 @@ internal sealed class SendStates {
|
||||||
val walletBalance: TextReference,
|
val walletBalance: TextReference,
|
||||||
val tokenIconState: TokenIconState,
|
val tokenIconState: TokenIconState,
|
||||||
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||||
|
val isSegmentedButtonsEnabled: Boolean,
|
||||||
val amountTextField: SendTextField.AmountField,
|
val amountTextField: SendTextField.AmountField,
|
||||||
val notifications: ImmutableList<SendNotification>,
|
|
||||||
val appCurrencyCode: String,
|
val appCurrencyCode: String,
|
||||||
val isFeeLoading: Boolean,
|
|
||||||
val subtractedFee: BigDecimal?,
|
|
||||||
) : SendStates()
|
) : SendStates()
|
||||||
|
|
||||||
/** Recipient state */
|
/** Recipient state */
|
||||||
|
|
@ -89,7 +87,6 @@ internal sealed class SendStates {
|
||||||
override val isPrimaryButtonEnabled: Boolean = true,
|
override val isPrimaryButtonEnabled: Boolean = true,
|
||||||
val isSending: Boolean,
|
val isSending: Boolean,
|
||||||
val isSuccess: Boolean,
|
val isSuccess: Boolean,
|
||||||
val isSubtract: Boolean,
|
|
||||||
val transactionDate: Long,
|
val transactionDate: Long,
|
||||||
val txUrl: String,
|
val txUrl: String,
|
||||||
val ignoreAmountReduce: Boolean,
|
val ignoreAmountReduce: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
package com.tangem.features.send.impl.presentation.state.amount
|
package com.tangem.features.send.impl.presentation.state.amount
|
||||||
|
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
import com.tangem.features.send.impl.presentation.state.SendNotification
|
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldChangeConverter
|
||||||
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
|
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldMaxAmountConverter
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory to produce amount state for [SendUiState]
|
* Factory to produce amount state for [SendUiState]
|
||||||
|
|
@ -43,18 +41,4 @@ internal class AmountStateFactory(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
fun getOnCurrencyChangedState(isFiat: Boolean) = amountCurrencyConverter.convert(isFiat)
|
||||||
|
|
||||||
fun getAmountNotificationState(notifications: ImmutableList<SendNotification>): SendUiState {
|
|
||||||
val state = currentStateProvider()
|
|
||||||
return state.copy(
|
|
||||||
amountState = state.amountState?.copy(notifications = notifications),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getOnAmountFeeLoadingCancel(): SendUiState {
|
|
||||||
val state = currentStateProvider()
|
|
||||||
return state.copy(
|
|
||||||
amountState = state.amountState?.copy(isFeeLoading = false),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -31,6 +31,7 @@ internal class SendAmountStateConverter(
|
||||||
val status = cryptoCurrencyStatusProvider()
|
val status = cryptoCurrencyStatusProvider()
|
||||||
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol)
|
||||||
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals)
|
||||||
|
val noFeeRate = status.value.fiatRate.isNullOrZero()
|
||||||
|
|
||||||
return SendStates.AmountState(
|
return SendStates.AmountState(
|
||||||
walletName = userWallet.name,
|
walletName = userWallet.name,
|
||||||
|
|
@ -38,26 +39,24 @@ internal class SendAmountStateConverter(
|
||||||
tokenIconState = iconStateConverter.convert(status),
|
tokenIconState = iconStateConverter.convert(status),
|
||||||
amountTextField = sendAmountFieldConverter.convert(value),
|
amountTextField = sendAmountFieldConverter.convert(value),
|
||||||
isPrimaryButtonEnabled = false,
|
isPrimaryButtonEnabled = false,
|
||||||
notifications = persistentListOf(),
|
|
||||||
isFeeLoading = false,
|
|
||||||
appCurrencyCode = appCurrency.code,
|
appCurrencyCode = appCurrency.code,
|
||||||
subtractedFee = null,
|
segmentedButtonConfig = persistentListOf(
|
||||||
segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) {
|
SendAmountSegmentedButtonsConfig(
|
||||||
persistentListOf()
|
title = stringReference(status.currency.symbol),
|
||||||
} else {
|
iconState = iconStateConverter.convertCustom(
|
||||||
persistentListOf(
|
value = status,
|
||||||
SendAmountSegmentedButtonsConfig(
|
forceGrayscale = noFeeRate,
|
||||||
title = stringReference(status.currency.symbol),
|
showCustomTokenBadge = false,
|
||||||
iconState = iconStateConverter.convert(status),
|
|
||||||
isFiat = false,
|
|
||||||
),
|
),
|
||||||
SendAmountSegmentedButtonsConfig(
|
isFiat = false,
|
||||||
title = stringReference(appCurrency.code),
|
),
|
||||||
iconUrl = appCurrency.iconSmallUrl,
|
SendAmountSegmentedButtonsConfig(
|
||||||
isFiat = true,
|
title = stringReference(appCurrency.code),
|
||||||
),
|
iconUrl = appCurrency.iconSmallUrl,
|
||||||
)
|
isFiat = true,
|
||||||
},
|
),
|
||||||
|
),
|
||||||
|
isSegmentedButtonsEnabled = !noFeeRate,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
package com.tangem.features.send.impl.presentation.state.amount
|
|
||||||
|
|
||||||
import com.tangem.core.ui.utils.parseBigDecimal
|
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
|
||||||
import com.tangem.utils.Provider
|
|
||||||
import com.tangem.utils.converter.Converter
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
internal class SendAmountSubtractConverter(
|
|
||||||
private val currentStateProvider: Provider<SendUiState>,
|
|
||||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
|
||||||
) : Converter<Unit, SendUiState> {
|
|
||||||
override fun convert(value: Unit): SendUiState {
|
|
||||||
val state = currentStateProvider()
|
|
||||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
|
||||||
val amountState = state.amountState ?: return state
|
|
||||||
val feeState = state.feeState ?: return state
|
|
||||||
val feeValue = feeState.fee?.amount?.value ?: return state
|
|
||||||
val amountTextField = amountState.amountTextField
|
|
||||||
val amountValue = amountTextField.cryptoAmount.value ?: return state
|
|
||||||
val fiatRate = cryptoCurrencyStatus.value.fiatRate
|
|
||||||
val cryptoDecimals = amountTextField.cryptoAmount.decimals
|
|
||||||
val fiatDecimals = amountTextField.fiatAmount.decimals
|
|
||||||
|
|
||||||
val feeDiff = amountState.subtractedFee?.let { feeValue.minus(it) } ?: feeValue
|
|
||||||
val decimalCryptoValue = amountValue.minus(feeDiff)
|
|
||||||
|
|
||||||
if (decimalCryptoValue < BigDecimal.ZERO) return state
|
|
||||||
|
|
||||||
val decimalFiatValue = decimalCryptoValue.multiply(fiatRate)
|
|
||||||
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
|
|
||||||
val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals)
|
|
||||||
|
|
||||||
return state.copy(
|
|
||||||
sendState = state.sendState?.copy(isSubtract = true),
|
|
||||||
amountState = amountState.copy(
|
|
||||||
subtractedFee = feeValue,
|
|
||||||
amountTextField = amountTextField.copy(
|
|
||||||
value = cryptoValue,
|
|
||||||
fiatValue = fiatValue,
|
|
||||||
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
|
|
||||||
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,6 @@ internal class SendConfirmStateConverter(
|
||||||
isPrimaryButtonEnabled = true,
|
isPrimaryButtonEnabled = true,
|
||||||
isSending = false,
|
isSending = false,
|
||||||
isSuccess = false,
|
isSuccess = false,
|
||||||
isSubtract = false,
|
|
||||||
transactionDate = 0L,
|
transactionDate = 0L,
|
||||||
txUrl = "",
|
txUrl = "",
|
||||||
ignoreAmountReduce = false,
|
ignoreAmountReduce = false,
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,11 @@ import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.features.send.impl.R
|
import com.tangem.features.send.impl.R
|
||||||
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
|
||||||
import com.tangem.features.send.impl.presentation.state.*
|
import com.tangem.features.send.impl.presentation.state.*
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
|
import com.tangem.features.send.impl.presentation.state.fee.*
|
||||||
import com.tangem.features.send.impl.presentation.state.fee.FeeType
|
|
||||||
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||||
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
|
import com.tangem.lib.crypto.BlockchainUtils.isDogecoin
|
||||||
|
import com.tangem.lib.crypto.BlockchainUtils.isTezos
|
||||||
import com.tangem.utils.Provider
|
import com.tangem.utils.Provider
|
||||||
import com.tangem.utils.isNullOrZero
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
@ -31,17 +30,16 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.BigInteger
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
internal class SendNotificationFactory(
|
internal class SendNotificationFactory(
|
||||||
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||||
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
private val coinCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
|
||||||
private val feePaidCryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
|
|
||||||
private val currentStateProvider: Provider<SendUiState>,
|
private val currentStateProvider: Provider<SendUiState>,
|
||||||
private val userWalletProvider: Provider<UserWallet>,
|
private val userWalletProvider: Provider<UserWallet>,
|
||||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||||
private val stateRouterProvider: Provider<StateRouter>,
|
private val stateRouterProvider: Provider<StateRouter>,
|
||||||
|
private val isSubtractAvailableProvider: Provider<Boolean>,
|
||||||
private val clickIntents: SendClickIntents,
|
private val clickIntents: SendClickIntents,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase,
|
||||||
|
|
@ -53,24 +51,39 @@ internal class SendNotificationFactory(
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
val sendState = state.sendState ?: return@map persistentListOf()
|
val sendState = state.sendState ?: return@map persistentListOf()
|
||||||
val feeState = state.feeState ?: return@map persistentListOf()
|
val feeState = state.feeState ?: return@map persistentListOf()
|
||||||
val feeAmount = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO
|
||||||
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
val amountValue = state.amountState?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO
|
||||||
|
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO
|
||||||
|
val isFeeCoverage = checkFeeCoverage(
|
||||||
|
isSubtractAvailable = isSubtractAvailableProvider(),
|
||||||
|
balance = balance,
|
||||||
|
amountValue = amountValue,
|
||||||
|
feeValue = feeValue,
|
||||||
|
)
|
||||||
|
val sendingAmount = calculateSubtractedAmount(
|
||||||
|
isFeeCoverage = isFeeCoverage,
|
||||||
|
cryptoCurrencyStatus = cryptoCurrencyStatusProvider(),
|
||||||
|
amountValue = amountValue,
|
||||||
|
feeValue = feeValue,
|
||||||
|
)
|
||||||
buildList {
|
buildList {
|
||||||
// errors
|
// errors
|
||||||
addExceedBalanceNotification(feeAmount, amountValue)
|
addFeeUnreachableNotification(feeState.feeSelectorState)
|
||||||
|
addExceedBalanceNotification(feeValue, sendingAmount)
|
||||||
addExceedsBalanceNotification(feeState.fee)
|
addExceedsBalanceNotification(feeState.fee)
|
||||||
addMinimumAmountErrorNotification(feeAmount, amountValue)
|
addMinimumAmountErrorNotification(feeValue, sendingAmount)
|
||||||
addDustWarningNotification(feeAmount, amountValue)
|
addDustWarningNotification(feeValue, sendingAmount)
|
||||||
addTransactionLimitErrorNotification(feeAmount, amountValue)
|
addTransactionLimitErrorNotification(feeValue, sendingAmount)
|
||||||
// warnings
|
// warnings
|
||||||
addExistentialWarningNotification(feeAmount, amountValue)
|
addExistentialWarningNotification(feeValue, amountValue)
|
||||||
addHighFeeWarningNotification(feeAmount, amountValue, sendState.ignoreAmountReduce)
|
addFeeCoverageNotification(isFeeCoverage)
|
||||||
|
addHighFeeWarningNotification(amountValue, sendState.ignoreAmountReduce)
|
||||||
addTooHighNotification(feeState.feeSelectorState)
|
addTooHighNotification(feeState.feeSelectorState)
|
||||||
addTooLowNotification(feeState)
|
addTooLowNotification(feeState)
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissNotificationState(clazz: Class<out SendNotification>): SendUiState {
|
fun dismissNotificationState(clazz: Class<out SendNotification>, isIgnored: Boolean = false): SendUiState {
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
val sendState = state.sendState ?: return state
|
val sendState = state.sendState ?: return state
|
||||||
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
|
val notificationsToRemove = sendState.notifications.filterIsInstance(clazz)
|
||||||
|
|
@ -78,50 +91,33 @@ internal class SendNotificationFactory(
|
||||||
updatedNotifications.removeAll(notificationsToRemove)
|
updatedNotifications.removeAll(notificationsToRemove)
|
||||||
return state.copy(
|
return state.copy(
|
||||||
sendState = sendState.copy(
|
sendState = sendState.copy(
|
||||||
ignoreAmountReduce = true,
|
ignoreAmountReduce = isIgnored,
|
||||||
notifications = updatedNotifications.toImmutableList(),
|
notifications = updatedNotifications.toImmutableList(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MutableList<SendNotification>.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) {
|
||||||
|
if (feeSelectorState is FeeSelectorState.Error) {
|
||||||
|
add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
private fun MutableList<SendNotification>.addExceedBalanceNotification(
|
||||||
feeAmount: BigDecimal,
|
feeAmount: BigDecimal,
|
||||||
receivedAmount: BigDecimal,
|
receivedAmount: BigDecimal,
|
||||||
) {
|
) {
|
||||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||||
val feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatusProvider()
|
|
||||||
val cryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
|
||||||
val coinCryptoAmount = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
|
||||||
|
|
||||||
val showNotification = if (cryptoCurrencyStatus.currency.id == feePaidCryptoCurrencyStatus?.currency?.id) {
|
if (!isSubtractAvailableProvider()) return
|
||||||
receivedAmount > cryptoAmount || feeAmount > coinCryptoAmount
|
|
||||||
} else {
|
|
||||||
receivedAmount + feeAmount > cryptoAmount
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val showNotification = receivedAmount + feeAmount > balance
|
||||||
if (showNotification) {
|
if (showNotification) {
|
||||||
add(SendNotification.Error.TotalExceedsBalance)
|
add(SendNotification.Error.TotalExceedsBalance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
|
|
||||||
feeAmount: BigDecimal,
|
|
||||||
receivedAmount: BigDecimal,
|
|
||||||
) {
|
|
||||||
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
|
||||||
|
|
||||||
val totalAmount = feeAmount + receivedAmount
|
|
||||||
val balance = coinCryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
|
||||||
|
|
||||||
if (isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)) {
|
|
||||||
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
|
||||||
if (receivedAmount < minimum || balance - totalAmount < minimum) {
|
|
||||||
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo temporarily disabling notification
|
// todo temporarily disabling notification
|
||||||
// private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(recipientAddress: String) {
|
// private suspend fun MutableList<SendNotification>.addReserveAmountErrorNotification(recipientAddress: String) {
|
||||||
// val userWalletId = userWalletProvider().walletId
|
// val userWalletId = userWalletProvider().walletId
|
||||||
|
|
@ -198,7 +194,7 @@ internal class SendNotificationFactory(
|
||||||
if (currencyDeposit != null && currencyDeposit > diff) {
|
if (currencyDeposit != null && currencyDeposit > diff) {
|
||||||
add(
|
add(
|
||||||
SendNotification.Error.ExistentialDeposit(
|
SendNotification.Error.ExistentialDeposit(
|
||||||
BigDecimalFormatter.formatCryptoAmount(
|
BigDecimalFormatter.formatCryptoAmountUncapped(
|
||||||
cryptoAmount = currencyDeposit,
|
cryptoAmount = currencyDeposit,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
),
|
),
|
||||||
|
|
@ -207,16 +203,22 @@ internal class SendNotificationFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun MutableList<SendNotification>.addFeeCoverageNotification(sendingAmount: Boolean) {
|
||||||
|
if (sendingAmount) {
|
||||||
|
analyticsEventHandler.send(SendAnalyticEvents.NoticeFeeCoverage)
|
||||||
|
add(SendNotification.Warning.FeeCoverageNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
private fun MutableList<SendNotification>.addHighFeeWarningNotification(
|
||||||
feeAmount: BigDecimal,
|
|
||||||
sendAmount: BigDecimal,
|
sendAmount: BigDecimal,
|
||||||
ignoreAmountReduce: Boolean,
|
ignoreAmountReduce: Boolean,
|
||||||
) {
|
) {
|
||||||
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||||
val isTezos = cryptoCurrencyStatus.currency.network.id.value == Blockchain.Tezos.id
|
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value)
|
||||||
val threshold = Blockchain.Tezos.minimalAmount()
|
val threshold = Blockchain.Tezos.minimalAmount()
|
||||||
val isTotalBalance = feeAmount.plus(sendAmount) >= balance && balance > threshold
|
val isTotalBalance = sendAmount >= balance && balance > threshold
|
||||||
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
if (!ignoreAmountReduce && isTotalBalance && isTezos) {
|
||||||
add(
|
add(
|
||||||
SendNotification.Warning.HighFeeError(
|
SendNotification.Warning.HighFeeError(
|
||||||
|
|
@ -233,6 +235,21 @@ internal class SendNotificationFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo remove in [REDACTED_TASK_KEY]
|
||||||
|
private fun MutableList<SendNotification>.addMinimumAmountErrorNotification(
|
||||||
|
feeAmount: BigDecimal,
|
||||||
|
receivedAmount: BigDecimal,
|
||||||
|
) {
|
||||||
|
val coinCryptoCurrencyStatus = coinCryptoCurrencyStatusProvider()
|
||||||
|
val minimum = BigDecimal(DOGECOIN_MINIMUM)
|
||||||
|
|
||||||
|
val isDogecoin = isDogecoin(coinCryptoCurrencyStatus.currency.network.id.value)
|
||||||
|
val isExceedDustLimit = checkDustLimits(feeAmount, receivedAmount, minimum)
|
||||||
|
if (isDogecoin && isExceedDustLimit) {
|
||||||
|
add(SendNotification.Error.MinimumAmountError(DOGECOIN_MINIMUM))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
|
private suspend fun MutableList<SendNotification>.addDustWarningNotification(
|
||||||
feeAmount: BigDecimal,
|
feeAmount: BigDecimal,
|
||||||
receivedAmount: BigDecimal,
|
receivedAmount: BigDecimal,
|
||||||
|
|
@ -241,18 +258,12 @@ internal class SendNotificationFactory(
|
||||||
val dustValue = currencyChecksRepository.getDustValue(
|
val dustValue = currencyChecksRepository.getDustValue(
|
||||||
userWalletProvider().walletId,
|
userWalletProvider().walletId,
|
||||||
cryptoCurrencyStatus.currency.network,
|
cryptoCurrencyStatus.currency.network,
|
||||||
)
|
) ?: return
|
||||||
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
|
||||||
if (dustValue != null && !balance.isNullOrZero() && receivedAmount < balance) {
|
if (checkDustLimits(feeAmount, receivedAmount, dustValue)) {
|
||||||
val totalAmount = feeAmount + receivedAmount
|
add(
|
||||||
val change = balance - totalAmount
|
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
|
||||||
val isChangeLowerThanDust = change < dustValue && change != BigDecimal.ZERO
|
)
|
||||||
val isShowWarning = totalAmount < dustValue || isChangeLowerThanDust
|
|
||||||
if (isShowWarning) {
|
|
||||||
add(
|
|
||||||
SendNotification.Error.MinimumAmountError(dustValue.toPlainString()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,13 +285,9 @@ internal class SendNotificationFactory(
|
||||||
|
|
||||||
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
private fun MutableList<SendNotification>.addTooHighNotification(feeSelectorState: FeeSelectorState) {
|
||||||
if (feeSelectorState !is FeeSelectorState.Content) return
|
if (feeSelectorState !is FeeSelectorState.Content) return
|
||||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return
|
|
||||||
val highValue = multipleFees.priority.amount.value ?: return
|
checkIfFeeTooHigh(feeSelectorState) { diff ->
|
||||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return
|
add(SendNotification.Warning.TooHigh(diff))
|
||||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
|
||||||
val diff = (customValue / highValue).toBigInteger()
|
|
||||||
if (feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF) {
|
|
||||||
add(SendNotification.Warning.TooHigh(diff.toString()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,8 +368,17 @@ internal class SendNotificationFactory(
|
||||||
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
|
return Blockchain.fromNetworkId(this.currency.network.backendId) == Blockchain.Arbitrum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean {
|
||||||
|
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
|
||||||
|
val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
|
||||||
|
|
||||||
|
val totalAmount = feeAmount + receivedAmount
|
||||||
|
val change = balance - totalAmount
|
||||||
|
val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO
|
||||||
|
return receivedAmount < dustValue || isChangeLowerThanDust
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val DOGECOIN_MINIMUM = "0.01"
|
private const val DOGECOIN_MINIMUM = "0.01"
|
||||||
internal val FEE_MAX_DIFF = BigInteger("5")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,73 @@
|
||||||
package com.tangem.features.send.impl.presentation.state.fee
|
package com.tangem.features.send.impl.presentation.state.fee
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||||
|
import com.tangem.blockchainsdk.utils.minimalAmount
|
||||||
|
import com.tangem.core.ui.utils.parseBigDecimal
|
||||||
import com.tangem.core.ui.utils.parseToBigDecimal
|
import com.tangem.core.ui.utils.parseToBigDecimal
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
import com.tangem.features.send.impl.presentation.state.SendUiState
|
import com.tangem.features.send.impl.presentation.state.SendUiState
|
||||||
import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory
|
import com.tangem.lib.crypto.BlockchainUtils
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if sending amount with fee is greater than balance
|
* Check and calculates subtracted amount
|
||||||
*/
|
*/
|
||||||
internal fun checkFeeCoverage(state: SendUiState, cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean {
|
internal fun checkAndCalculateSubtractedAmount(
|
||||||
val balance = cryptoCurrencyStatus.value.amount ?: return false
|
isAmountSubtractAvailable: Boolean,
|
||||||
val fee = state.feeState?.fee?.amount?.value ?: return false
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
val amount = state.amountState?.amountTextField?.cryptoAmount?.value ?: return false
|
amountValue: BigDecimal,
|
||||||
return balance < amount + fee && balance > fee
|
feeValue: BigDecimal,
|
||||||
|
): BigDecimal {
|
||||||
|
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||||
|
val isFeeCoverage = checkFeeCoverage(
|
||||||
|
isSubtractAvailable = isAmountSubtractAvailable,
|
||||||
|
balance = balance,
|
||||||
|
amountValue = amountValue,
|
||||||
|
feeValue = feeValue,
|
||||||
|
)
|
||||||
|
return calculateSubtractedAmount(
|
||||||
|
isFeeCoverage = isFeeCoverage,
|
||||||
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
|
amountValue = amountValue,
|
||||||
|
feeValue = feeValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sending amount with fee is greater than balance
|
||||||
|
*/
|
||||||
|
internal fun checkFeeCoverage(
|
||||||
|
isSubtractAvailable: Boolean,
|
||||||
|
balance: BigDecimal,
|
||||||
|
amountValue: BigDecimal,
|
||||||
|
feeValue: BigDecimal,
|
||||||
|
): Boolean {
|
||||||
|
if (!isSubtractAvailable) return false
|
||||||
|
return balance < amountValue + feeValue && balance > feeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates subtracted amount
|
||||||
|
*/
|
||||||
|
internal fun calculateSubtractedAmount(
|
||||||
|
isFeeCoverage: Boolean,
|
||||||
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
|
amountValue: BigDecimal,
|
||||||
|
feeValue: BigDecimal,
|
||||||
|
): BigDecimal {
|
||||||
|
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
|
||||||
|
return if (isFeeCoverage) {
|
||||||
|
var subtractedValue = minOf(amountValue, balance.minus(feeValue))
|
||||||
|
if (BlockchainUtils.isTezos(cryptoCurrencyStatus.currency.network.id.value)) {
|
||||||
|
val threshold = Blockchain.Tezos.minimalAmount()
|
||||||
|
subtractedValue = -threshold
|
||||||
|
}
|
||||||
|
subtractedValue
|
||||||
|
} else {
|
||||||
|
amountValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,14 +86,16 @@ internal fun checkIfFeeTooLow(state: SendUiState): Boolean {
|
||||||
/**
|
/**
|
||||||
* Check if custom fee is too high
|
* Check if custom fee is too high
|
||||||
*/
|
*/
|
||||||
internal fun checkIfFeeTooHigh(state: SendUiState, onShow: (String) -> Unit): Boolean {
|
internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorState.Content, onShow: (String) -> Unit): Boolean {
|
||||||
val feeSelectorState = state.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
|
||||||
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return false
|
||||||
val highValue = multipleFees.priority.amount.value ?: return false
|
val highValue = multipleFees.priority.amount.value ?: return false
|
||||||
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
val customAmount = feeSelectorState.customValues.firstOrNull() ?: return false
|
||||||
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
|
||||||
val diff = (customValue / highValue).toBigInteger()
|
val diff = customValue / highValue
|
||||||
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > SendNotificationFactory.FEE_MAX_DIFF
|
val isShow = feeSelectorState.selectedFee == FeeType.Custom && diff > FEE_MAX_DIFF
|
||||||
if (isShow) onShow(diff.toString())
|
if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP))
|
||||||
return isShow
|
return isShow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val FEE_MAX_DIFF = BigDecimal("5")
|
||||||
|
private const val ZERO_DECIMALS = 0
|
||||||
|
|
@ -15,6 +15,8 @@ internal sealed class FeeSelectorState {
|
||||||
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
val customValues: ImmutableList<SendTextField.CustomFee> = persistentListOf(),
|
||||||
) : FeeSelectorState()
|
) : FeeSelectorState()
|
||||||
|
|
||||||
|
data object Loading : FeeSelectorState()
|
||||||
|
|
||||||
data object Error : FeeSelectorState()
|
data object Error : FeeSelectorState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,12 @@ internal class FeeStateFactory(
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
val feeState = state.feeState ?: return state
|
val feeState = state.feeState ?: return state
|
||||||
return state.copy(
|
return state.copy(
|
||||||
amountState = state.amountState?.copy(isFeeLoading = true),
|
|
||||||
feeState = feeState.copy(
|
feeState = feeState.copy(
|
||||||
|
feeSelectorState = if (feeState.feeSelectorState is FeeSelectorState.Content) {
|
||||||
|
feeState.feeSelectorState
|
||||||
|
} else {
|
||||||
|
FeeSelectorState.Loading
|
||||||
|
},
|
||||||
notifications = persistentListOf(),
|
notifications = persistentListOf(),
|
||||||
isPrimaryButtonEnabled = false,
|
isPrimaryButtonEnabled = false,
|
||||||
),
|
),
|
||||||
|
|
@ -73,7 +77,6 @@ internal class FeeStateFactory(
|
||||||
|
|
||||||
val fee = feeConverter.convert(updatedFeeSelectorState)
|
val fee = feeConverter.convert(updatedFeeSelectorState)
|
||||||
return state.copy(
|
return state.copy(
|
||||||
amountState = state.amountState?.copy(isFeeLoading = false),
|
|
||||||
feeState = feeState.copy(
|
feeState = feeState.copy(
|
||||||
feeSelectorState = updatedFeeSelectorState,
|
feeSelectorState = updatedFeeSelectorState,
|
||||||
fee = fee,
|
fee = fee,
|
||||||
|
|
@ -85,7 +88,6 @@ internal class FeeStateFactory(
|
||||||
fun onFeeOnErrorState(): SendUiState {
|
fun onFeeOnErrorState(): SendUiState {
|
||||||
val state = currentStateProvider()
|
val state = currentStateProvider()
|
||||||
return state.copy(
|
return state.copy(
|
||||||
amountState = state.amountState?.copy(isFeeLoading = false),
|
|
||||||
feeState = state.feeState?.copy(
|
feeState = state.feeState?.copy(
|
||||||
feeSelectorState = FeeSelectorState.Error,
|
feeSelectorState = FeeSelectorState.Error,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ internal class SendFeeStateConverter(
|
||||||
|
|
||||||
override fun convert(value: Unit): SendStates.FeeState {
|
override fun convert(value: Unit): SendStates.FeeState {
|
||||||
return SendStates.FeeState(
|
return SendStates.FeeState(
|
||||||
feeSelectorState = FeeSelectorState.Error,
|
feeSelectorState = FeeSelectorState.Loading,
|
||||||
fee = null,
|
fee = null,
|
||||||
notifications = persistentListOf(),
|
notifications = persistentListOf(),
|
||||||
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate,
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ internal class SendAmountFieldChangeConverter(
|
||||||
return state.copy(
|
return state.copy(
|
||||||
amountState = amountState.copy(
|
amountState = amountState.copy(
|
||||||
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
|
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
|
||||||
subtractedFee = null,
|
|
||||||
amountTextField = amountTextField.copy(
|
amountTextField = amountTextField.copy(
|
||||||
value = cryptoValue,
|
value = cryptoValue,
|
||||||
fiatValue = fiatValue,
|
fiatValue = fiatValue,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ internal class SendAmountFieldMaxAmountConverter(
|
||||||
return state.copy(
|
return state.copy(
|
||||||
amountState = amountState.copy(
|
amountState = amountState.copy(
|
||||||
isPrimaryButtonEnabled = true,
|
isPrimaryButtonEnabled = true,
|
||||||
subtractedFee = null,
|
|
||||||
amountTextField = amountTextField.copy(
|
amountTextField = amountTextField.copy(
|
||||||
value = cryptoValue,
|
value = cryptoValue,
|
||||||
fiatValue = fiatValue,
|
fiatValue = fiatValue,
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,7 @@ internal object AmountStatePreviewData {
|
||||||
walletBalance = stringReference("123.123"),
|
walletBalance = stringReference("123.123"),
|
||||||
tokenIconState = TokenIconState.Loading,
|
tokenIconState = TokenIconState.Loading,
|
||||||
segmentedButtonConfig = persistentListOf(),
|
segmentedButtonConfig = persistentListOf(),
|
||||||
notifications = persistentListOf(),
|
|
||||||
isFeeLoading = false,
|
|
||||||
appCurrencyCode = "usd",
|
appCurrencyCode = "usd",
|
||||||
subtractedFee = null,
|
|
||||||
amountTextField = SendTextField.AmountField(
|
amountTextField = SendTextField.AmountField(
|
||||||
value = "123.123123123123123123",
|
value = "123.123123123123123123",
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
|
@ -49,6 +46,7 @@ internal object AmountStatePreviewData {
|
||||||
isError = false,
|
isError = false,
|
||||||
error = TextReference.EMPTY,
|
error = TextReference.EMPTY,
|
||||||
),
|
),
|
||||||
|
isSegmentedButtonsEnabled = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
val fiatAmountState = amountState.copy(
|
val fiatAmountState = amountState.copy(
|
||||||
|
|
|
||||||
|
|
@ -34,14 +34,12 @@ internal object SendClickIntentsStub : SendClickIntents {
|
||||||
|
|
||||||
override fun onRecipientMemoValueChange(value: String) {}
|
override fun onRecipientMemoValueChange(value: String) {}
|
||||||
|
|
||||||
override fun feeReload(isToNextState: Boolean) {}
|
override fun feeReload() {}
|
||||||
|
|
||||||
override fun onFeeSelectorClick(feeType: FeeType) {}
|
override fun onFeeSelectorClick(feeType: FeeType) {}
|
||||||
|
|
||||||
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
override fun onCustomFeeValueChange(index: Int, value: String) {}
|
||||||
|
|
||||||
override fun onSubtractSelect() {}
|
|
||||||
|
|
||||||
override fun onReadMoreClick() {}
|
override fun onReadMoreClick() {}
|
||||||
|
|
||||||
override fun onSendClick() {}
|
override fun onSendClick() {}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||||
import com.tangem.core.ui.utils.toDateFormat
|
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||||
import com.tangem.core.ui.utils.toTimeFormat
|
import com.tangem.core.ui.utils.toTimeFormat
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
|
|
@ -81,7 +81,7 @@ internal class SendRecipientHistoryListConverter(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
private fun TxHistoryItem.extractTimestamp(): TextReference {
|
||||||
val date = timestampInMillis.toDateFormat(
|
val date = timestampInMillis.toDateFormatWithTodayYesterday(
|
||||||
formatter = DateTimeFormatters.dateDDMMYYYY,
|
formatter = DateTimeFormatters.dateDDMMYYYY,
|
||||||
)
|
)
|
||||||
val time = timestampInMillis.toTimeFormat()
|
val time = timestampInMillis.toTimeFormat()
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,25 @@ internal class SendRecipientWalletListConverter :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<AvailableWallet?>.filterWallets() = this.filterNotNull()
|
private fun List<AvailableWallet?>.filterWallets(): PersistentList<SendRecipientListContent> {
|
||||||
.groupBy { item -> item.name }
|
var walletsCounter = 0
|
||||||
.values.map {
|
return this.filterNotNull()
|
||||||
it.mapIndexed { index, item ->
|
.groupBy { item -> item.name }
|
||||||
val name = if (it.size > 1) {
|
.values.map {
|
||||||
"${item.name} ${index.inc()}"
|
it.mapIndexed { index, item ->
|
||||||
} else {
|
val name = if (it.size > 1) {
|
||||||
item.name
|
"${item.name} ${index.inc()}"
|
||||||
|
} else {
|
||||||
|
item.name
|
||||||
|
}
|
||||||
|
SendRecipientListContent(
|
||||||
|
id = "${WALLET_KEY_TAG}${walletsCounter++}",
|
||||||
|
title = TextReference.Str(item.address),
|
||||||
|
subtitle = TextReference.Str(name),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
SendRecipientListContent(
|
|
||||||
id = "${WALLET_KEY_TAG}$index",
|
|
||||||
title = TextReference.Str(item.address),
|
|
||||||
subtitle = TextReference.Str(name),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
.flatten()
|
||||||
.flatten()
|
.toPersistentList()
|
||||||
.toPersistentList()
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,6 @@ private fun SendNavigationButton(
|
||||||
val isFromConfirmation = currentState.isFromConfirmation
|
val isFromConfirmation = currentState.isFromConfirmation
|
||||||
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
|
val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee
|
||||||
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
|
val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess && !isSending
|
||||||
val showProgress = uiState.amountState?.isFeeLoading == true
|
|
||||||
|
|
||||||
val (buttonTextId, buttonClick) = getButtonData(
|
val (buttonTextId, buttonClick) = getButtonData(
|
||||||
currentState = currentState,
|
currentState = currentState,
|
||||||
|
|
@ -120,7 +119,7 @@ private fun SendNavigationButton(
|
||||||
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
buttonClick()
|
buttonClick()
|
||||||
},
|
},
|
||||||
showProgress = showProgress,
|
showProgress = false,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
colors = TangemButtonsDefaults.primaryButtonColors,
|
colors = TangemButtonsDefaults.primaryButtonColors,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
|
||||||
internal fun LazyListScope.buttons(
|
internal fun LazyListScope.buttons(
|
||||||
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
|
||||||
clickIntents: SendClickIntents,
|
clickIntents: SendClickIntents,
|
||||||
isMaxButtonEnabled: Boolean,
|
isSegmentedButtonsEnabled: Boolean,
|
||||||
) {
|
) {
|
||||||
item(
|
item(
|
||||||
key = AMOUNT_BUTTONS_KEY,
|
key = AMOUNT_BUTTONS_KEY,
|
||||||
|
|
@ -48,15 +48,18 @@ internal fun LazyListScope.buttons(
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
clickIntents.onCurrencyChangeClick(it.isFiat)
|
clickIntents.onCurrencyChangeClick(it.isFiat)
|
||||||
},
|
},
|
||||||
|
isEnabled = isSegmentedButtonsEnabled,
|
||||||
) {
|
) {
|
||||||
SendAmountCurrencyButton(it)
|
SendAmountCurrencyButton(
|
||||||
|
button = it,
|
||||||
|
isSegmentedButtonsEnabled = isSegmentedButtonsEnabled,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
SpacerWMax()
|
SpacerWMax()
|
||||||
}
|
}
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
text = stringResource(R.string.send_max_amount),
|
text = stringResource(R.string.send_max_amount),
|
||||||
enabled = isMaxButtonEnabled,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
clickIntents.onMaxValueClick()
|
clickIntents.onMaxValueClick()
|
||||||
|
|
@ -72,7 +75,7 @@ internal fun LazyListScope.buttons(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -86,17 +89,16 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) {
|
||||||
FiatIcon(
|
FiatIcon(
|
||||||
url = button.iconUrl,
|
url = button.iconUrl,
|
||||||
size = TangemTheme.dimens.size18,
|
size = TangemTheme.dimens.size18,
|
||||||
|
isGrayscale = !isSegmentedButtonsEnabled,
|
||||||
modifier = Modifier.size(TangemTheme.dimens.size18),
|
modifier = Modifier.size(TangemTheme.dimens.size18),
|
||||||
)
|
)
|
||||||
} else {
|
} else if (button.iconState != null) {
|
||||||
button.iconState?.let {
|
TokenIcon(
|
||||||
TokenIcon(
|
state = button.iconState,
|
||||||
state = it,
|
shouldDisplayNetwork = false,
|
||||||
shouldDisplayNetwork = false,
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.size(TangemTheme.dimens.size18),
|
||||||
.size(TangemTheme.dimens.size18),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = button.title.resolveReference(),
|
text = button.title.resolveReference(),
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,10 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||||
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.job
|
import kotlinx.coroutines.job
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolean, appCurrencyCode: String) {
|
internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) {
|
||||||
val decimalFormat = rememberDecimalFormat()
|
val decimalFormat = rememberDecimalFormat()
|
||||||
val isFiatValue = sendField.isFiatValue
|
val isFiatValue = sendField.isFiatValue
|
||||||
val currencyCode = if (isFiatValue) appCurrencyCode else null
|
val currencyCode = if (isFiatValue) appCurrencyCode else null
|
||||||
|
|
@ -36,15 +35,6 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
||||||
sendField.cryptoAmount to sendField.value
|
sendField.cryptoAmount to sendField.value
|
||||||
}
|
}
|
||||||
val requester = remember { FocusRequester() }
|
val requester = remember { FocusRequester() }
|
||||||
var isEnabledProxy by remember { mutableStateOf(isEnabled) }
|
|
||||||
|
|
||||||
// Fix animation from amount screen to summary screen ([REDACTED_TASK_KEY])
|
|
||||||
LaunchedEffect(key1 = isEnabled) {
|
|
||||||
if (isEnabled) {
|
|
||||||
delay(timeMillis = 700)
|
|
||||||
}
|
|
||||||
isEnabledProxy = isEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
AmountTextField(
|
AmountTextField(
|
||||||
value = primaryValue,
|
value = primaryValue,
|
||||||
|
|
@ -62,7 +52,6 @@ internal fun AmountField(sendField: SendTextField.AmountField, isEnabled: Boolea
|
||||||
color = TangemTheme.colors.text.primary1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
),
|
),
|
||||||
isEnabled = isEnabledProxy,
|
|
||||||
isAutoResize = true,
|
isAutoResize = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(requester)
|
.focusRequester(requester)
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ internal fun LazyListScope.amountField(
|
||||||
)
|
)
|
||||||
AmountField(
|
AmountField(
|
||||||
sendField = amountState.amountTextField,
|
sendField = amountState.amountTextField,
|
||||||
isEnabled = !amountState.isFeeLoading,
|
|
||||||
appCurrencyCode = amountState.appCurrencyCode,
|
appCurrencyCode = amountState.appCurrencyCode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
import com.tangem.features.send.impl.presentation.state.SendStates
|
||||||
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
|
import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData
|
||||||
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
|
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
|
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -35,9 +34,8 @@ internal fun SendAmountContent(
|
||||||
buttons(
|
buttons(
|
||||||
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
segmentedButtonConfig = amountState.segmentedButtonConfig,
|
||||||
clickIntents = clickIntents,
|
clickIntents = clickIntents,
|
||||||
isMaxButtonEnabled = !amountState.isFeeLoading,
|
isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled,
|
||||||
)
|
)
|
||||||
notifications(amountState.notifications)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
package com.tangem.features.send.impl.presentation.ui.send
|
package com.tangem.features.send.impl.presentation.ui.send
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import com.tangem.core.ui.components.RectangleShimmer
|
||||||
import com.tangem.core.ui.components.rows.SelectorRowItem
|
import com.tangem.core.ui.components.rows.SelectorRowItem
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||||
import com.tangem.features.send.impl.R
|
import com.tangem.features.send.impl.R
|
||||||
import com.tangem.features.send.impl.presentation.state.SendStates
|
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.FeeSelectorState
|
||||||
|
|
@ -26,20 +27,12 @@ import com.tangem.features.send.impl.presentation.utils.getFiatReference
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
|
internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) {
|
||||||
val fee = feeState.fee ?: return
|
|
||||||
val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return
|
|
||||||
val (title, icon) = 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_fee_selector_option_custom to R.drawable.ic_edit_24
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
.background(TangemTheme.colors.background.action)
|
.background(TangemTheme.colors.background.action)
|
||||||
.clickable(enabled = !isSuccess) { onClick() }
|
.clickable(enabled = !isSuccess && feeState.fee != null) { onClick() }
|
||||||
.padding(TangemTheme.dimens.spacing12),
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -47,17 +40,71 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick
|
||||||
style = TangemTheme.typography.caption2,
|
style = TangemTheme.typography.caption2,
|
||||||
color = TangemTheme.colors.text.secondary,
|
color = TangemTheme.colors.text.secondary,
|
||||||
)
|
)
|
||||||
SelectorRowItem(
|
|
||||||
titleRes = title,
|
Box(
|
||||||
iconRes = icon,
|
|
||||||
preDot = getCryptoReference(fee.amount, feeState.isFeeApproximate),
|
|
||||||
postDot = getFiatReference(fee.amount, feeState.rate, feeState.appCurrency),
|
|
||||||
ellipsizeOffset = fee.amount.currencySymbol.length,
|
|
||||||
isSelected = true,
|
|
||||||
showDivider = false,
|
|
||||||
paddingValues = PaddingValues(),
|
|
||||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
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_fee_selector_option_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 = getCryptoReference(feeAmount, feeState.isFeeApproximate),
|
||||||
|
postDot = feeAmount?.let { getFiatReference(it, feeState.rate, feeState.appCurrency) },
|
||||||
|
ellipsizeOffset = feeAmount?.currencySymbol?.length,
|
||||||
|
isSelected = true,
|
||||||
|
showDivider = 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 == FeeSelectorState.Error) {
|
||||||
|
Text(
|
||||||
|
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
style = TangemTheme.typography.body2,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -40,6 +41,7 @@ internal fun RecipientBlock(
|
||||||
.padding(TangemTheme.dimens.spacing12),
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
) {
|
) {
|
||||||
AddressBlock(recipientState.addressTextField)
|
AddressBlock(recipientState.addressTextField)
|
||||||
|
MemoBlock(recipientState.memoTextField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +72,28 @@ private fun AddressBlock(address: SendTextField.RecipientAddress) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
// region Preview
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -39,14 +39,12 @@ internal interface SendClickIntents {
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Fee
|
// region Fee
|
||||||
fun feeReload(isToNextState: Boolean = false)
|
fun feeReload()
|
||||||
|
|
||||||
fun onFeeSelectorClick(feeType: FeeType)
|
fun onFeeSelectorClick(feeType: FeeType)
|
||||||
|
|
||||||
fun onCustomFeeValueChange(index: Int, value: String)
|
fun onCustomFeeValueChange(index: Int, value: String)
|
||||||
|
|
||||||
fun onSubtractSelect()
|
|
||||||
|
|
||||||
fun onReadMoreClick()
|
fun onReadMoreClick()
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.tangem.domain.tokens.*
|
||||||
import com.tangem.domain.tokens.error.CurrencyStatusError
|
import com.tangem.domain.tokens.error.CurrencyStatusError
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.tokens.model.Network
|
||||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||||
import com.tangem.domain.tokens.utils.convertToAmount
|
import com.tangem.domain.tokens.utils.convertToAmount
|
||||||
import com.tangem.domain.transaction.error.GetFeeError
|
import com.tangem.domain.transaction.error.GetFeeError
|
||||||
|
|
@ -156,10 +157,10 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val sendNotificationFactory = SendNotificationFactory(
|
private val sendNotificationFactory = SendNotificationFactory(
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus },
|
||||||
feePaidCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState },
|
||||||
userWalletProvider = Provider { userWallet },
|
userWalletProvider = Provider { userWallet },
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
|
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
||||||
currencyChecksRepository = currencyChecksRepository,
|
currencyChecksRepository = currencyChecksRepository,
|
||||||
clickIntents = this,
|
clickIntents = this,
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
|
|
@ -191,7 +192,6 @@ internal class SendViewModel @Inject constructor(
|
||||||
private var qrScannerJobHolder = JobHolder()
|
private var qrScannerJobHolder = JobHolder()
|
||||||
|
|
||||||
private var sendIdleTimer = 0L
|
private var sendIdleTimer = 0L
|
||||||
private var feeIdleTimer = 0L
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
subscribeOnCurrencyStatusUpdates()
|
subscribeOnCurrencyStatusUpdates()
|
||||||
|
|
@ -359,13 +359,14 @@ internal class SendViewModel @Inject constructor(
|
||||||
stateRouter.showSend()
|
stateRouter.showSend()
|
||||||
}
|
}
|
||||||
transactionId != null && amount != null && destinationAddress != null -> {
|
transactionId != null && amount != null && destinationAddress != null -> {
|
||||||
|
loadFee()
|
||||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||||
stateRouter.showFee()
|
stateRouter.showSend()
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
getWalletsAndRecent()
|
|
||||||
uiState = stateFactory.getReadyState()
|
uiState = stateFactory.getReadyState()
|
||||||
|
getWalletsAndRecent()
|
||||||
stateRouter.showRecipient()
|
stateRouter.showRecipient()
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
}
|
}
|
||||||
|
|
@ -406,7 +407,8 @@ internal class SendViewModel @Inject constructor(
|
||||||
return if (!isMultiCurrency) {
|
return if (!isMultiCurrency) {
|
||||||
val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull()
|
val status = getCryptoCurrencyStatusSyncUseCase(walletId).getOrNull()
|
||||||
val address = status?.value?.networkAddress.takeIf {
|
val address = status?.value?.networkAddress.takeIf {
|
||||||
status?.currency?.network?.id == cryptoCurrency.network.id
|
status?.currency?.network?.id == cryptoCurrency.network.id &&
|
||||||
|
status.currency.network.derivationPath !is Network.DerivationPath.Custom
|
||||||
}
|
}
|
||||||
address?.let {
|
address?.let {
|
||||||
AvailableWallet(
|
AvailableWallet(
|
||||||
|
|
@ -417,7 +419,8 @@ internal class SendViewModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull()
|
val statuses = getCryptoCurrencyStatusesSyncUseCase(walletId).getOrNull()
|
||||||
val walletCurrency = statuses?.firstOrNull {
|
val walletCurrency = statuses?.firstOrNull {
|
||||||
it.currency.network.id == cryptoCurrency.network.id
|
it.currency.network.id == cryptoCurrency.network.id &&
|
||||||
|
it.currency.network.derivationPath !is Network.DerivationPath.Custom
|
||||||
}
|
}
|
||||||
val address = walletCurrency?.value?.networkAddress
|
val address = walletCurrency?.value?.networkAddress
|
||||||
address?.let {
|
address?.let {
|
||||||
|
|
@ -484,12 +487,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
if (onFeeNext()) return
|
if (onFeeNext()) return
|
||||||
}
|
}
|
||||||
SendUiStateType.Amount -> {
|
SendUiStateType.Amount -> {
|
||||||
if (uiState.feeState?.feeSelectorState is FeeSelectorState.Content) {
|
loadFee()
|
||||||
if (onFeeCoverageAlert()) return
|
|
||||||
} else {
|
|
||||||
loadFee(isToNextState = true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
}
|
}
|
||||||
|
|
@ -515,15 +513,15 @@ internal class SendViewModel @Inject constructor(
|
||||||
innerRouter.openTokenDetails(userWalletId, currency)
|
innerRouter.openTokenDetails(userWalletId, currency)
|
||||||
|
|
||||||
private fun onFeeNext(): Boolean {
|
private fun onFeeNext(): Boolean {
|
||||||
if (onFeeCoverageAlert()) return true
|
|
||||||
if (checkIfFeeTooLow(uiState)) {
|
if (checkIfFeeTooLow(uiState)) {
|
||||||
uiState = eventStateFactory.getFeeTooLowAlert(
|
uiState = eventStateFactory.getFeeTooLowAlert(
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
val feeSelectorState = uiState.feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||||
return checkIfFeeTooHigh(
|
return checkIfFeeTooHigh(
|
||||||
state = uiState,
|
feeSelectorState = feeSelectorState,
|
||||||
onShow = { diff ->
|
onShow = { diff ->
|
||||||
uiState = eventStateFactory.getFeeTooHighAlert(
|
uiState = eventStateFactory.getFeeTooHighAlert(
|
||||||
diff = diff,
|
diff = diff,
|
||||||
|
|
@ -533,19 +531,6 @@ internal class SendViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeeCoverageAlert(): Boolean {
|
|
||||||
val isFeeCoverage = checkFeeCoverage(uiState, cryptoCurrencyStatus)
|
|
||||||
return if (isAmountSubtractAvailable && isFeeCoverage) {
|
|
||||||
uiState = eventStateFactory.getFeeCoverageAlert(
|
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
|
||||||
)
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false))
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelFeeRequest() {
|
private fun cancelFeeRequest() {
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
feeJobHolder.cancel()
|
feeJobHolder.cancel()
|
||||||
|
|
@ -647,7 +632,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region fee
|
// region fee
|
||||||
override fun feeReload(isToNextState: Boolean) = loadFee(isToNextState = isToNextState)
|
override fun feeReload() = loadFee()
|
||||||
|
|
||||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||||
|
|
@ -662,12 +647,6 @@ internal class SendViewModel @Inject constructor(
|
||||||
updateFeeNotifications()
|
updateFeeNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSubtractSelect() {
|
|
||||||
uiState = stateFactory.onSubtractSelect(isAmountSubtractAvailable)
|
|
||||||
stateRouter.showSend()
|
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReadMoreClick() {
|
override fun onReadMoreClick() {
|
||||||
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
|
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
|
||||||
val url = buildString {
|
val url = buildString {
|
||||||
|
|
@ -678,10 +657,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
innerRouter.openUrl(url)
|
innerRouter.openUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFee(isToNextState: Boolean = false) {
|
private fun loadFee() {
|
||||||
// debouncing fee request
|
|
||||||
if (SystemClock.elapsedRealtime() - feeIdleTimer < FEE_UPDATE_DELAY) return
|
|
||||||
|
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
val isShowStatus = uiState.feeState?.fee == null
|
val isShowStatus = uiState.feeState?.fee == null
|
||||||
if (isShowStatus) {
|
if (isShowStatus) {
|
||||||
|
|
@ -689,35 +665,25 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
val result = callFeeUseCase()?.fold(
|
val result = callFeeUseCase()?.fold(
|
||||||
ifRight = {
|
ifRight = {
|
||||||
feeIdleTimer = SystemClock.elapsedRealtime()
|
|
||||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
||||||
if (isToNextState && !onFeeCoverageAlert()) {
|
|
||||||
stateRouter.showSend()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
onFeeLoadFailed(isShowStatus, isToNextState)
|
onFeeLoadFailed(isShowStatus)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
onFeeLoadFailed(isShowStatus, isToNextState)
|
onFeeLoadFailed(isShowStatus)
|
||||||
}
|
}
|
||||||
|
updateNotifications()
|
||||||
updateFeeNotifications()
|
updateFeeNotifications()
|
||||||
}.saveIn(feeJobHolder)
|
}.saveIn(feeJobHolder)
|
||||||
.invokeOnCompletion {
|
.invokeOnCompletion {
|
||||||
uiState = amountStateFactory.getOnAmountFeeLoadingCancel()
|
// todo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeeLoadFailed(isShowStatus: Boolean, isToNextState: Boolean) {
|
private fun onFeeLoadFailed(isShowStatus: Boolean) {
|
||||||
when {
|
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
|
||||||
isToNextState -> {
|
|
||||||
uiState = eventStateFactory.getFeeUnreachableErrorState {
|
|
||||||
uiState = eventStateFactory.onConsumeEventState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isShowStatus -> uiState = feeStateFactory.onFeeOnErrorState()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkIfSubtractAvailable() {
|
private suspend fun checkIfSubtractAvailable() {
|
||||||
|
|
@ -735,7 +701,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
return getFeeUseCase.invoke(
|
return getFeeUseCase.invoke(
|
||||||
amount = amount,
|
amount = amount,
|
||||||
destination = recipientState.addressTextField.value,
|
destination = recipientState.addressTextField.value,
|
||||||
userWalletId = userWalletId,
|
userWallet = userWallet,
|
||||||
cryptoCurrency = cryptoCurrency,
|
cryptoCurrency = cryptoCurrency,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -751,7 +717,6 @@ internal class SendViewModel @Inject constructor(
|
||||||
verifyAndSendTransaction()
|
verifyAndSendTransaction()
|
||||||
} else {
|
} else {
|
||||||
onCheckFeeUpdate()
|
onCheckFeeUpdate()
|
||||||
feeIdleTimer = SystemClock.elapsedRealtime()
|
|
||||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -792,10 +757,11 @@ internal class SendViewModel @Inject constructor(
|
||||||
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
|
override fun onAmountReduceClick(reducedAmount: BigDecimal, clazz: Class<out SendNotification>) {
|
||||||
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
|
uiState = amountStateFactory.getOnAmountValueChange(reducedAmount.parseBigDecimal(cryptoCurrency.decimals))
|
||||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
||||||
|
feeReload()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
||||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyAndSendTransaction() {
|
private fun verifyAndSendTransaction() {
|
||||||
|
|
@ -804,10 +770,18 @@ internal class SendViewModel @Inject constructor(
|
||||||
val fee = feeState.fee ?: return
|
val fee = feeState.fee ?: return
|
||||||
val memo = uiState.recipientState?.memoTextField?.value
|
val memo = uiState.recipientState?.memoTextField?.value
|
||||||
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return
|
||||||
|
val feeValue = fee.amount.value ?: return
|
||||||
|
|
||||||
|
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||||
|
isAmountSubtractAvailable = isAmountSubtractAvailable,
|
||||||
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
|
amountValue = amountValue,
|
||||||
|
feeValue = feeValue,
|
||||||
|
)
|
||||||
|
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
createTransactionUseCase(
|
createTransactionUseCase(
|
||||||
amount = amountValue.convertToAmount(cryptoCurrency),
|
amount = receivingAmount.convertToAmount(cryptoCurrency),
|
||||||
fee = fee,
|
fee = fee,
|
||||||
memo = memo,
|
memo = memo,
|
||||||
destination = recipient,
|
destination = recipient,
|
||||||
|
|
@ -920,7 +894,6 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val CHECK_FEE_UPDATE_DELAY = 60_000L
|
const val CHECK_FEE_UPDATE_DELAY = 60_000L
|
||||||
const val FEE_UPDATE_DELAY = 10_000L
|
|
||||||
const val BALANCE_UPDATE_DELAY = 10_000L
|
const val BALANCE_UPDATE_DELAY = 10_000L
|
||||||
|
|
||||||
const val RU_LOCALE = "ru"
|
const val RU_LOCALE = "ru"
|
||||||
|
|
|
||||||
|
|
@ -124,14 +124,15 @@ class SwapDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSendTransactionUseCase(
|
fun provideSendTransactionUseCase(
|
||||||
@SwapScope isDemoCardUseCase: IsDemoCardUseCase,
|
|
||||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
transactionRepository: TransactionRepository,
|
transactionRepository: TransactionRepository,
|
||||||
|
walletManagersFacade: WalletManagersFacade,
|
||||||
): SendTransactionUseCase {
|
): SendTransactionUseCase {
|
||||||
return SendTransactionUseCase(
|
return SendTransactionUseCase(
|
||||||
isDemoCardUseCase = isDemoCardUseCase,
|
demoConfig = DemoConfig(),
|
||||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||||
transactionRepository = transactionRepository,
|
transactionRepository = transactionRepository,
|
||||||
|
walletManagersFacade = walletManagersFacade,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -646,6 +646,7 @@ internal class StateBuilder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning {
|
private fun getWarningForError(dataError: DataError, fromToken: CryptoCurrency): SwapWarning {
|
||||||
|
val providerErrorMessage = getProviderErrorMessage(dataError)
|
||||||
return when (dataError) {
|
return when (dataError) {
|
||||||
is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
|
is DataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError(
|
||||||
notificationConfig = NotificationConfig(
|
notificationConfig = NotificationConfig(
|
||||||
|
|
@ -674,10 +675,16 @@ internal class StateBuilder(
|
||||||
} else {
|
} else {
|
||||||
resourceReference(R.string.warning_express_refresh_required_title)
|
resourceReference(R.string.warning_express_refresh_required_title)
|
||||||
},
|
},
|
||||||
subtitle = if (dataError is DataError.UnknownError) {
|
subtitle = when {
|
||||||
resourceReference(R.string.common_unknown_error)
|
dataError is DataError.UnknownError -> {
|
||||||
} else {
|
resourceReference(R.string.common_unknown_error)
|
||||||
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
}
|
||||||
|
providerErrorMessage != null -> {
|
||||||
|
providerErrorMessage
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
iconResId = R.drawable.img_attention_20,
|
iconResId = R.drawable.img_attention_20,
|
||||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||||
|
|
@ -972,7 +979,7 @@ internal class StateBuilder(
|
||||||
return uiState.copy(
|
return uiState.copy(
|
||||||
alert = SwapWarning.GenericWarning(
|
alert = SwapWarning.GenericWarning(
|
||||||
message = if (swapTransactionState is SwapTransactionState.ExpressError) {
|
message = if (swapTransactionState is SwapTransactionState.ExpressError) {
|
||||||
getAlertErrorMessage(swapTransactionState.dataError)
|
getProviderErrorMessage(swapTransactionState.dataError)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
|
@ -987,7 +994,7 @@ internal class StateBuilder(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlertErrorMessage(dataError: DataError): TextReference? {
|
private fun getProviderErrorMessage(dataError: DataError): TextReference? {
|
||||||
return when (dataError) {
|
return when (dataError) {
|
||||||
is DataError.SwapsAreUnavailableNowError -> resourceReference(
|
is DataError.SwapsAreUnavailableNowError -> resourceReference(
|
||||||
id = R.string.express_error_swap_unavailable,
|
id = R.string.express_error_swap_unavailable,
|
||||||
|
|
@ -998,6 +1005,7 @@ internal class StateBuilder(
|
||||||
formatArgs = wrappedList(dataError.code),
|
formatArgs = wrappedList(dataError.code),
|
||||||
)
|
)
|
||||||
is DataError.ExchangeProviderNotActiveError,
|
is DataError.ExchangeProviderNotActiveError,
|
||||||
|
is DataError.ExchangeProviderNotFoundError,
|
||||||
is DataError.ExchangeProviderNotAvailableError,
|
is DataError.ExchangeProviderNotAvailableError,
|
||||||
is DataError.ExchangeProviderProviderInternalError,
|
is DataError.ExchangeProviderProviderInternalError,
|
||||||
-> resourceReference(
|
-> resourceReference(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||||
import com.tangem.core.ui.utils.toDateFormat
|
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||||
import com.tangem.core.ui.utils.toTimeFormat
|
import com.tangem.core.ui.utils.toTimeFormat
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
|
@ -71,7 +71,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
||||||
txUrl = transaction.status?.txExternalUrl,
|
txUrl = transaction.status?.txExternalUrl,
|
||||||
txExternalId = transaction.status?.txExternalId,
|
txExternalId = transaction.status?.txExternalId,
|
||||||
timestamp = TextReference.Str(
|
timestamp = TextReference.Str(
|
||||||
"${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}",
|
"${timestamp.toDateFormatWithTodayYesterday()}, ${timestamp.toTimeFormat()}",
|
||||||
),
|
),
|
||||||
fiatSymbol = appCurrency.symbol,
|
fiatSymbol = appCurrency.symbol,
|
||||||
statuses = getStatuses(transaction.status?.status),
|
statuses = getStatuses(transaction.status?.status),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.paging.*
|
||||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||||
import com.tangem.core.ui.utils.toDateFormat
|
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||||
import com.tangem.domain.txhistory.models.TxHistoryItem
|
import com.tangem.domain.txhistory.models.TxHistoryItem
|
||||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||||
|
|
@ -68,7 +68,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
||||||
// Use raw timestamp to get date
|
// Use raw timestamp to get date
|
||||||
|
|
||||||
// If [afterDate] is the first transaction in the flow, add the group title
|
// If [afterDate] is the first transaction in the flow, add the group title
|
||||||
val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
val afterDate = after.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||||
if (before is TxHistoryItemState.Title) {
|
if (before is TxHistoryItemState.Title) {
|
||||||
return@insertSeparators TxHistoryItemState.GroupTitle(
|
return@insertSeparators TxHistoryItemState.GroupTitle(
|
||||||
title = afterDate,
|
title = afterDate,
|
||||||
|
|
@ -80,7 +80,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter(
|
||||||
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
||||||
* the new group
|
* the new group
|
||||||
*/
|
*/
|
||||||
val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
val beforeDate = before.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||||
return@insertSeparators if (beforeDate != afterDate) {
|
return@insertSeparators if (beforeDate != afterDate) {
|
||||||
TxHistoryItemState.GroupTitle(
|
TxHistoryItemState.GroupTitle(
|
||||||
title = afterDate,
|
title = afterDate,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import com.tangem.blockchain.common.address.AddressType
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||||
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
|
|
||||||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
|
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
|
@ -166,7 +165,6 @@ internal class TokenDetailsViewModel @Inject constructor(
|
||||||
viewModel = this,
|
viewModel = this,
|
||||||
deepLinks = listOf(
|
deepLinks = listOf(
|
||||||
BuyCurrencyDeepLink(::onBuyCurrencyDeepLink),
|
BuyCurrencyDeepLink(::onBuyCurrencyDeepLink),
|
||||||
SellCurrencyDeepLink(::onSellCurrencyDeepLink),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -176,20 +174,6 @@ internal class TokenDetailsViewModel @Inject constructor(
|
||||||
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol))
|
analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSellCurrencyDeepLink(data: SellCurrencyDeepLink.Data) {
|
|
||||||
sendCurrency(
|
|
||||||
status = cryptoCurrencyStatus ?: return,
|
|
||||||
transactionInfo = data.let {
|
|
||||||
TransactionInfo(
|
|
||||||
transactionId = it.transactionId,
|
|
||||||
destinationAddress = it.depositWalletAddress,
|
|
||||||
amount = it.baseCurrencyAmount,
|
|
||||||
tag = it.depositWalletAddressTag,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
analyticsEventsHandler.send(
|
analyticsEventsHandler.send(
|
||||||
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol),
|
event = TokenScreenAnalyticsEvent.DetailsScreenOpened(token = cryptoCurrency.symbol),
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,14 @@ import com.tangem.core.deeplink.DeepLinksRegistry
|
||||||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||||
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
|
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
|
||||||
import com.tangem.domain.redux.ReduxStateHolder
|
import com.tangem.domain.redux.ReduxStateHolder
|
||||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
|
import com.tangem.domain.tokens.*
|
||||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
|
||||||
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
|
|
||||||
import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase
|
|
||||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
|
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -29,23 +27,20 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
|
||||||
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
|
||||||
|
private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||||
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
|
||||||
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
|
||||||
private val reduxStateHolder: ReduxStateHolder,
|
private val reduxStateHolder: ReduxStateHolder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var deepLinks: List<DeepLink> = emptyList()
|
private var deepLinksMap = mutableMapOf<UserWalletId, List<DeepLink>>()
|
||||||
|
|
||||||
fun registerForSingleCurrencyWallets(viewModel: ViewModel, userWallet: UserWallet) {
|
fun registerForWallet(viewModel: ViewModel, userWallet: UserWallet) {
|
||||||
if (userWallet.isMultiCurrency) {
|
val deepLinks = deepLinksMap.getOrPut(userWallet.walletId) {
|
||||||
deepLinksRegistry.unregister(deepLinks)
|
getDeepLinks(userWallet, viewModel.viewModelScope)
|
||||||
} else {
|
|
||||||
if (deepLinks.isEmpty()) {
|
|
||||||
deepLinks = getDeepLinks(userWallet, viewModel.viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
deepLinksRegistry.register(deepLinks)
|
|
||||||
}
|
}
|
||||||
|
deepLinksRegistry.unregisterByIds(deepLinks.map { it.id })
|
||||||
|
deepLinksRegistry.register(deepLinks)
|
||||||
|
|
||||||
viewModel.addCloseable {
|
viewModel.addCloseable {
|
||||||
deepLinksRegistry.unregister(deepLinks)
|
deepLinksRegistry.unregister(deepLinks)
|
||||||
|
|
@ -60,20 +55,23 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val buyCurrencyDeepLink = BuyCurrencyDeepLink(
|
|
||||||
onReceive = {
|
|
||||||
scope.launch {
|
|
||||||
onBuyCurrencyDeepLink(userWallet)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return listOf(sellCurrencyDeepLink, buyCurrencyDeepLink)
|
return buildList {
|
||||||
|
add(sellCurrencyDeepLink)
|
||||||
|
if (!userWallet.isMultiCurrency) {
|
||||||
|
add(
|
||||||
|
BuyCurrencyDeepLink(
|
||||||
|
onReceive = {
|
||||||
|
scope.launch { onBuyCurrencyDeepLink(userWallet) }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) {
|
private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) {
|
||||||
val cryptoCurrencyStatus = getCryptoCurrencyStatusSyncUseCase(userWallet.walletId)
|
val cryptoCurrencyStatus = findCryptoCurrencyStatus(userWallet, data.currencyId) ?: return
|
||||||
.getOrNull() ?: return
|
|
||||||
val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
|
||||||
userWallet.walletId,
|
userWallet.walletId,
|
||||||
cryptoCurrencyStatus,
|
cryptoCurrencyStatus,
|
||||||
|
|
@ -110,6 +108,19 @@ internal class WalletDeepLinksHandler @Inject constructor(
|
||||||
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun findCryptoCurrencyStatus(
|
||||||
|
userWallet: UserWallet,
|
||||||
|
currencyIdValue: String,
|
||||||
|
): CryptoCurrencyStatus? {
|
||||||
|
return if (userWallet.isMultiCurrency) {
|
||||||
|
getCryptoCurrencyStatusesSyncUseCase(userWallet.walletId).getOrNull()?.let { currencies ->
|
||||||
|
currencies.find { currencyIdValue == it.currency.id.value }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getCryptoCurrencyStatusSyncUseCase(userWallet.walletId).getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendCoin(
|
private fun sendCoin(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.paging.*
|
||||||
import com.tangem.core.ui.components.transactions.state.TransactionState
|
import com.tangem.core.ui.components.transactions.state.TransactionState
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState.TxHistoryItemState
|
||||||
import com.tangem.core.ui.utils.toDateFormat
|
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
|
||||||
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
@ -50,7 +50,7 @@ internal class TxHistoryItemFlowConverter(
|
||||||
// Use raw timestamp to get date
|
// Use raw timestamp to get date
|
||||||
|
|
||||||
// If [afterDate] is the first transaction in the flow, add the group title
|
// If [afterDate] is the first transaction in the flow, add the group title
|
||||||
val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
val afterDate = after.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||||
if (before is TxHistoryItemState.Title) {
|
if (before is TxHistoryItemState.Title) {
|
||||||
return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
return@insertSeparators TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ internal class TxHistoryItemFlowConverter(
|
||||||
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
* If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in
|
||||||
* the new group
|
* the new group
|
||||||
*/
|
*/
|
||||||
val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null
|
val beforeDate = before.getTimestamp()?.toDateFormatWithTodayYesterday() ?: return@insertSeparators null
|
||||||
return@insertSeparators if (beforeDate != afterDate) {
|
return@insertSeparators if (beforeDate != afterDate) {
|
||||||
TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
TxHistoryItemState.GroupTitle(afterDate, itemKey = UUID.randomUUID().toString())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ internal class WalletViewModel @Inject constructor(
|
||||||
selectedWalletAnalyticsSender.send(selectedWallet)
|
selectedWalletAnalyticsSender.send(selectedWallet)
|
||||||
}
|
}
|
||||||
|
|
||||||
walletDeepLinksHandler.registerForSingleCurrencyWallets(
|
walletDeepLinksHandler.registerForWallet(
|
||||||
viewModel = this,
|
viewModel = this,
|
||||||
userWallet = selectedWallet,
|
userWallet = selectedWallet,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,15 @@ object BlockchainUtils {
|
||||||
return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
|
return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** If current [networkId] is Dogecoin */
|
||||||
fun isDogecoin(networkId: String): Boolean {
|
fun isDogecoin(networkId: String): Boolean {
|
||||||
val blockchain = Blockchain.fromId(networkId)
|
val blockchain = Blockchain.fromId(networkId)
|
||||||
return blockchain == Blockchain.Dogecoin
|
return blockchain == Blockchain.Dogecoin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** If current [networkId] is Tezos */
|
||||||
|
fun isTezos(networkId: String): Boolean {
|
||||||
|
val blockchain = Blockchain.fromId(networkId)
|
||||||
|
return blockchain == Blockchain.Tezos
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue