Updated on 2026-08-14
This commit is contained in:
commit
8301a40879
39 changed files with 1855 additions and 663 deletions
|
|
@ -52,4 +52,19 @@ fun BigDecimal.toFiatString(rateValue: BigDecimal): String? {
|
|||
return "≈ USD $fiatValue"
|
||||
}
|
||||
|
||||
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
|
||||
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
|
||||
|
||||
fun BigDecimal.isPositive(): Boolean = this.compareTo(BigDecimal.ZERO) == 1
|
||||
fun BigDecimal.isNegative(): Boolean = this.compareTo(BigDecimal.ZERO) == -1
|
||||
fun BigDecimal.isGreaterThan(value: BigDecimal): Boolean = this.compareTo(value) == 1
|
||||
fun BigDecimal.isLessThan(value: BigDecimal): Boolean = this.compareTo(value) == -1
|
||||
|
||||
fun BigDecimal.isGreaterThanOrEqual(value: BigDecimal): Boolean {
|
||||
val compareResult = this.compareTo(value)
|
||||
return compareResult == 1 || compareResult == 0
|
||||
}
|
||||
|
||||
fun BigDecimal.isLessThanOrEqual(value: BigDecimal): Boolean {
|
||||
val compareResult = this.compareTo(value)
|
||||
return compareResult == -1 || compareResult == 0
|
||||
}
|
||||
|
|
@ -27,10 +27,20 @@ fun EditText.update(text: String?) {
|
|||
else setSelection(cursorPosition)
|
||||
}
|
||||
|
||||
fun TextSwitcher.update(text: String?) {
|
||||
fun EditText.setOnImeActionListener(action: Int, handler: (EditText) -> Unit) {
|
||||
this.setOnEditorActionListener { view, actionId, event ->
|
||||
if (actionId == action) {
|
||||
handler.invoke(this)
|
||||
return@setOnEditorActionListener true
|
||||
}
|
||||
return@setOnEditorActionListener false
|
||||
}
|
||||
}
|
||||
|
||||
fun TextSwitcher.update(text: CharSequence?) {
|
||||
val textView = this.currentView as? TextView ?: return
|
||||
|
||||
if (textView.text?.toString() != text) this.setText(text)
|
||||
if (textView.text?.toString() != text?.toString()) this.setText(text)
|
||||
}
|
||||
|
||||
// By default the TextInputLayout didn't activates the error state if the message is empty or null
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.fragment.app.Fragment
|
||||
|
|
@ -27,17 +28,26 @@ fun Context.getDrawableCompat(@DrawableRes drawableResId: Int): Drawable? {
|
|||
return ContextCompat.getDrawable(this, drawableResId)
|
||||
}
|
||||
|
||||
fun View.show(show: Boolean) {
|
||||
if (show) this.visibility = View.VISIBLE else this.visibility = View.GONE
|
||||
fun View.getString(@StringRes id: Int): String {
|
||||
return context.getString(id)
|
||||
}
|
||||
|
||||
fun View.show() {
|
||||
fun View.show(show: Boolean, invokeBeforeStateChanged: (() -> Unit)? = null) {
|
||||
return if (show) this.show(invokeBeforeStateChanged)
|
||||
else this.hide(invokeBeforeStateChanged)
|
||||
}
|
||||
|
||||
fun View.show(invokeBeforeStateChanged: (() -> Unit)? = null) {
|
||||
if (this.visibility == View.VISIBLE) return
|
||||
|
||||
invokeBeforeStateChanged?.invoke()
|
||||
this.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun View.hide() {
|
||||
fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
|
||||
if (this.visibility == View.GONE) return
|
||||
|
||||
invokeBeforeStateChanged?.invoke()
|
||||
this.visibility = View.GONE
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ package com.tangem.tap.common.extensions
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewParent
|
||||
import androidx.transition.AutoTransition
|
||||
import androidx.transition.Transition
|
||||
import androidx.transition.TransitionManager
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,6 +18,11 @@ fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGrou
|
|||
}
|
||||
}
|
||||
|
||||
fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()) {
|
||||
if (this == null) Timber.e("Can't invoke beginDelayedTransition, because parent is NULL")
|
||||
(this as? ViewGroup)?.beginDelayedTransition(transition)
|
||||
}
|
||||
|
||||
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
|
||||
TransitionManager.beginDelayedTransition(this, transition)
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.common.redux
|
|||
|
||||
import com.tangem.tap.common.redux.global.globalReducer
|
||||
import com.tangem.tap.common.redux.navigation.NavigationReducer
|
||||
import com.tangem.tap.features.send.redux.SendReducer
|
||||
import com.tangem.tap.features.send.redux.reducers.SendReducer
|
||||
import com.tangem.tap.features.wallet.redux.WalletReducer
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import com.tangem.tap.common.redux.global.GlobalState
|
|||
import com.tangem.tap.common.redux.navigation.NavigationState
|
||||
import com.tangem.tap.common.redux.navigation.navigationMiddleware
|
||||
import com.tangem.tap.features.home.redux.homeMiddleware
|
||||
import com.tangem.tap.features.send.redux.SendState
|
||||
import com.tangem.tap.features.send.redux.sendMiddleware
|
||||
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.features.wallet.redux.WalletState
|
||||
import com.tangem.tap.features.wallet.redux.walletMiddleware
|
||||
import org.rekotlin.Middleware
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.tap.domain
|
|||
|
||||
import androidx.activity.ComponentActivity
|
||||
import com.tangem.*
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.CardType
|
||||
|
|
@ -9,6 +11,7 @@ import com.tangem.tangem_sdk_new.extensions.init
|
|||
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
|
||||
import com.tangem.tap.domain.tasks.ScanNoteResponse
|
||||
import com.tangem.tap.domain.tasks.ScanNoteTask
|
||||
import com.tangem.tap.domain.tasks.SendTask
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
|
@ -28,6 +31,21 @@ class TangemSdkManager(val activity: ComponentActivity) {
|
|||
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask())
|
||||
}
|
||||
|
||||
suspend fun send(
|
||||
walletManager: WalletManager,
|
||||
recipientAddress: String,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount
|
||||
): CompletionResult<CommandResponse> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
suspendCoroutine { continuation ->
|
||||
tangemSdk.startSessionWithRunnable(SendTask(walletManager, recipientAddress, amountToSend, feeAmount)) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : CommandResponse> runTaskAsync(
|
||||
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
|
||||
): CompletionResult<T> =
|
||||
|
|
|
|||
|
|
@ -9,4 +9,6 @@ sealed class TapError(@StringRes val localizedMessage: Int): Throwable() {
|
|||
object PayIdEmptyField: TapError(R.string.wallet_create_payid_empty)
|
||||
object UnknownBlockchain: TapError(R.string.wallet_unknown_blockchain)
|
||||
object NoInternetConnection: TapError(R.string.notification_no_internet)
|
||||
object InsufficientBalance: TapError(R.string.error_insufficient_balance)
|
||||
object BlockchainInternalError: TapError(R.string.error_insufficient_balance)
|
||||
}
|
||||
114
app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt
Normal file
114
app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.tap.domain.tasks
|
||||
|
||||
import com.tangem.CardSession
|
||||
import com.tangem.CardSessionRunnable
|
||||
import com.tangem.TangemError
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.SignCommand
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class SendTask(
|
||||
private val walletManager: WalletManager,
|
||||
private val recipientAddress: String,
|
||||
private val amountToSend: Amount,
|
||||
private val feeAmount: Amount,
|
||||
) : CardSessionRunnable<CommandResponse> {
|
||||
|
||||
override val requiresPin2: Boolean = false
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<CommandResponse>) -> Unit) {
|
||||
val txSender = walletManager as TransactionSender
|
||||
|
||||
val verifyResult = walletManager.validateTransaction(amountToSend, feeAmount)
|
||||
if (verifyResult.isNotEmpty()) {
|
||||
callback(CompletionResult.Failure(InsufficientBalance()))
|
||||
return
|
||||
}
|
||||
val txData = walletManager.createTransaction(amountToSend, feeAmount, recipientAddress)
|
||||
|
||||
scope.launch {
|
||||
when (val result = txSender.send(txData, SessionTransactionSigner(session))) {
|
||||
is SimpleResult.Success -> callback(CompletionResult.Success(SendResponse()))
|
||||
is SimpleResult.Failure -> {
|
||||
callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(result.error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SendResponse : CommandResponse
|
||||
|
||||
class SessionTransactionSigner(
|
||||
private val session: CardSession
|
||||
) : TransactionSigner {
|
||||
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): CompletionResult<SignResponse> =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
Timber.d("sign transaction...")
|
||||
SignCommand(hashes).run(session) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SendError : TangemError {
|
||||
}
|
||||
|
||||
class UnknownError : SendError() {
|
||||
override val code: Int = 1000
|
||||
override var customMessage: String = "Unknown error"
|
||||
}
|
||||
|
||||
open class ThrowableError(throwable: Throwable?) : SendError() {
|
||||
override val code: Int = 1001
|
||||
override var customMessage: String = throwable?.localizedMessage ?: "Unknown exception"
|
||||
}
|
||||
|
||||
class InsufficientBalance(
|
||||
override var customMessage: String = "Insufficient balance"
|
||||
) : SendError() {
|
||||
override val code: Int = 1021
|
||||
}
|
||||
|
||||
class BlockchainInternalError(
|
||||
override var customMessage: String
|
||||
) : SendError() {
|
||||
override val code: Int = 2000
|
||||
}
|
||||
|
||||
class BlockchainInternalErrorConverter {
|
||||
companion object {
|
||||
|
||||
private val stellarInternalErrors = mapOf(
|
||||
"tx_bad_seq" to "Sequence number does not match source account",
|
||||
"tx_too_late" to "The ledger closeTime was after the maxTime",
|
||||
"tx_failedop_no_destination" to "The destination account does not exist",
|
||||
"tx_no_source_account" to "Source account not found"
|
||||
)
|
||||
|
||||
fun convert(throwable: Throwable?): TangemError {
|
||||
val message = throwable?.message ?: return ThrowableError(throwable)
|
||||
|
||||
val customMessage = getInternalBlockchainErrorMessage(message)
|
||||
return if (customMessage == null) ThrowableError(throwable)
|
||||
else BlockchainInternalError(customMessage)
|
||||
}
|
||||
|
||||
private fun getInternalBlockchainErrorMessage(message: String): String? {
|
||||
return stellarInternalErrors[message]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,15 @@
|
|||
package com.tangem.tap.features.send
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.android.synthetic.main.fragment_wallet.*
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
|
|
@ -16,6 +20,7 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
|
|||
|
||||
abstract fun subscribeToStore()
|
||||
|
||||
private lateinit var mainView: View
|
||||
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -27,6 +32,11 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
|
|||
})
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
mainView = super.onCreateView(inflater, container, savedInstanceState)!!
|
||||
return mainView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
toolbar.setNavigationOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
|
||||
|
|
@ -42,4 +52,12 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
|
|||
storeSubscribersList.forEach { store.unsubscribe(it) }
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
fun showRetrySnackbar(message: String, action: () -> Unit) {
|
||||
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
|
||||
snackbar.setAction(getString(R.string.generic_retry)) {
|
||||
snackbar.dismiss()
|
||||
action()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.commands.common.network.Result
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.PayIdManager
|
||||
import com.tangem.tap.domain.PayIdManager.Companion.isPayId
|
||||
import com.tangem.tap.domain.isPayIdSupported
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.ChangeAddressOrPayId
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.*
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
handleSendAction(action)
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendAction(action: Action) {
|
||||
val sendAction = action as? SendScreenActionUi ?: return
|
||||
|
||||
when (sendAction) {
|
||||
is AddressPayIdActionUi -> {
|
||||
when (sendAction) {
|
||||
is ChangeAddressOrPayId -> AddressPayIdHandler().handle(sendAction.data)
|
||||
}
|
||||
}
|
||||
is AmountActionUi -> {
|
||||
when (sendAction) {
|
||||
is ToggleMainCurrency -> {
|
||||
if (store.state.sendState.amountState.mainCurrency.value == MainCurrencyType.FIAT) {
|
||||
store.dispatch(SetMainCurrency(MainCurrencyType.CRYPTO))
|
||||
} else {
|
||||
store.dispatch(SetMainCurrency(MainCurrencyType.FIAT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class AddressPayIdHandler {
|
||||
fun handle(data: String) {
|
||||
val walletManager = store.state.globalState.scanNoteResponse?.walletManager ?: return
|
||||
if (data == store.state.sendState.addressPayIdState.etFieldValue) return
|
||||
|
||||
if (isPayId(data)) {
|
||||
verifyPayId(data, walletManager)
|
||||
} else {
|
||||
val supposedAddress = extractAddressFromShareUri(data)
|
||||
store.dispatch(PayIdVerification.SetError(supposedAddress, FailReason.IS_NOT_PAY_ID))
|
||||
verifyAddress(walletManager, supposedAddress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyPayId(payId: String, walletManager: WalletManager) {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
if (!blockchain.isPayIdSupported()) {
|
||||
store.dispatch(PayIdVerification.SetError(payId, FailReason.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val result = PayIdManager().verifyPayId(payId, blockchain)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val address = result.data.getAddress()
|
||||
if (address == null) {
|
||||
store.dispatch(PayIdVerification.SetError(payId, FailReason.PAY_ID_NOT_REGISTERED))
|
||||
return@withContext
|
||||
}
|
||||
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, address)
|
||||
|
||||
val actionToSend = if (failReason == FailReason.NONE) PayIdVerification.SetPayIdWalletAddress(payId, address)
|
||||
else AddressVerification.SetError(payId, failReason)
|
||||
store.dispatch(actionToSend)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
store.dispatch(PayIdVerification.SetError(payId, FailReason.PAY_ID_REQUEST_FAILED))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyAddress(walletManager: WalletManager, supposedAddress: String) {
|
||||
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, supposedAddress)
|
||||
val actionToSend = if (failReason == FailReason.NONE) AddressVerification.SetWalletAddress(supposedAddress)
|
||||
else AddressVerification.SetError(supposedAddress, failReason)
|
||||
|
||||
store.dispatch(actionToSend)
|
||||
}
|
||||
|
||||
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): FailReason {
|
||||
return if (wallet.blockchain.validateAddress(address)) {
|
||||
if (wallet.address != address) {
|
||||
FailReason.NONE
|
||||
} else {
|
||||
FailReason.ADDRESS_SAME_AS_WALLET
|
||||
}
|
||||
} else {
|
||||
FailReason.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: move to the blockchainSDK
|
||||
private fun extractAddressFromShareUri(shareUri: String): String {
|
||||
val sharePrefix = listOf("bitcoin:", "ethereum:", "ripple:")
|
||||
val prefixes = sharePrefix.filter { shareUri.contains(it) }
|
||||
return if (prefixes.isEmpty()) shareUri
|
||||
else shareUri.replace(prefixes[0], "")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux
|
||||
|
||||
import android.view.View
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.ChangeAmountToSend
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.*
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.StateType
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SendReducer {
|
||||
companion object {
|
||||
fun reduce(action: Action, sendState: SendState): SendState {
|
||||
val newState = internalReduce(action, sendState)
|
||||
if (newState == sendState) Timber.i("state didn't modified.")
|
||||
else Timber.i("state was updated to: $newState.")
|
||||
return newState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun internalReduce(incomingAction: Action, sendState: SendState): SendState {
|
||||
if (incomingAction is ReleaseSendState) return SendState()
|
||||
val action = incomingAction as? SendScreenAction ?: return sendState
|
||||
|
||||
var state = when (action) {
|
||||
is AddressPayIdActionUi -> handleAddressPayIdActionUi(action, sendState, sendState.addressPayIdState)
|
||||
is AddressPayIdVerifyAction -> handleAddressPayIdAction(action, sendState, sendState.addressPayIdState)
|
||||
is AmountActionUi -> handleAmountActionUi(action, sendState, sendState.amountState)
|
||||
is FeeActionUi -> handleFeeActionUi(action, sendState, sendState.feeLayoutState)
|
||||
else -> sendState
|
||||
}
|
||||
state = state.copy(sendButtonIsEnabled = state.addressPayIdState.error == null
|
||||
&& state.addressPayIdState.walletAddress?.isNotEmpty() ?: false
|
||||
&& !state.amountState.amountIsOverBalance
|
||||
&& !state.amountState.amountToSendCrypto.isZero()
|
||||
)
|
||||
return state
|
||||
}
|
||||
|
||||
fun handleAddressPayIdActionUi(
|
||||
action: AddressPayIdActionUi,
|
||||
sendState: SendState,
|
||||
state: AddressPayIdState
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is ChangeAddressOrPayId -> state
|
||||
is SetTruncateHandler -> state.copy(truncateHandler = action.handler)
|
||||
is TruncateOrRestore -> {
|
||||
if (action.truncate) state.copy(etFieldValue = state.truncatedFieldValue)
|
||||
else state.copy(etFieldValue = state.normalFieldValue)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(addressPayIdState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAddressPayIdAction(
|
||||
action: AddressPayIdVerifyAction,
|
||||
sendState: SendState,
|
||||
state: AddressPayIdState
|
||||
): SendState {
|
||||
val result = when (action) {
|
||||
is PayIdVerification.SetPayIdWalletAddress -> state.copyPayIdWalletAddress(action.payId, action.payIdWalletAddress)
|
||||
is PayIdVerification.SetError -> state.copyPaiIdError(action.payId, action.reason)
|
||||
is AddressVerification.SetWalletAddress -> state.copyWalletAddress(action.address)
|
||||
is AddressVerification.SetError -> state.copyError(action.address, action.reason)
|
||||
}
|
||||
return updateLastState(sendState.copy(addressPayIdState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAmountActionUi(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
|
||||
val rates = store.state.globalState.fiatRates
|
||||
val wallet = store.state.walletState.wallet ?: return sendState
|
||||
val fiatRate = rates.getRateForCryptoCurrency(wallet.blockchain.currency) ?: return sendState
|
||||
|
||||
val walletAmount = wallet.amounts[AmountType.Token] ?: wallet.amounts[AmountType.Coin]
|
||||
val converter = CurrencyConverter(fiatRate)
|
||||
|
||||
val result = when (action) {
|
||||
is SetMainCurrency -> {
|
||||
when (action.mainCurrency) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val fiatToSend = if (state.amountToSendCrypto.isZero()) BigDecimal.ZERO
|
||||
else converter.toFiat(state.amountToSendCrypto)
|
||||
|
||||
val fiatBalance = converter.toFiat(walletAmount?.value ?: BigDecimal.ZERO)
|
||||
|
||||
state.copy(
|
||||
etAmountFieldValue = fiatToSend.stripZeroPlainString(),
|
||||
balance = fiatBalance,
|
||||
mainCurrency = Value(MainCurrencyType.FIAT, TapCurrency.main),
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
val cryptoBalance = walletAmount?.value ?: BigDecimal.ZERO
|
||||
state.copy(
|
||||
etAmountFieldValue = state.amountToSendCrypto.stripZeroPlainString(),
|
||||
amountToSendCrypto = state.amountToSendCrypto,
|
||||
balance = cryptoBalance.stripTrailingZeros(),
|
||||
mainCurrency = Value(MainCurrencyType.CRYPTO, walletAmount?.currencySymbol ?: "null"),
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is AmountActionUi.SetMaxAmount -> {
|
||||
when (state.mainCurrency.value) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val cryptoBalance = walletAmount?.value ?: BigDecimal.ZERO
|
||||
val fiatToSend = converter.toFiat(cryptoBalance)
|
||||
|
||||
state.copy(
|
||||
etAmountFieldValue = fiatToSend.stripZeroPlainString(),
|
||||
amountToSendCrypto = cryptoBalance,
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
val cryptoBalance = walletAmount?.value ?: BigDecimal.ZERO
|
||||
state.copy(
|
||||
etAmountFieldValue = cryptoBalance.stripZeroPlainString(),
|
||||
amountToSendCrypto = cryptoBalance,
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
is ChangeAmountToSend -> {
|
||||
val bdData = when {
|
||||
action.data.isEmpty() || action.data == "0" -> BigDecimal.ZERO
|
||||
else -> BigDecimal(action.data)
|
||||
}
|
||||
|
||||
when (state.mainCurrency.value) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val sendCrypto = converter.toCrypto(bdData, wallet.blockchain.decimals()).stripTrailingZeros()
|
||||
state.copy(
|
||||
etAmountFieldValue = action.data,
|
||||
amountToSendCrypto = sendCrypto,
|
||||
amountIsOverBalance = state.balance < bdData,
|
||||
cursorAtTheSamePosition = true
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
state.copy(
|
||||
etAmountFieldValue = action.data,
|
||||
amountToSendCrypto = bdData,
|
||||
amountIsOverBalance = state.balance < bdData,
|
||||
cursorAtTheSamePosition = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
|
||||
private fun handleFeeActionUi(action: FeeActionUi, sendState: SendState, state: FeeLayoutState): SendState {
|
||||
val result = when (action) {
|
||||
is ToggleFeeLayoutVisibility -> {
|
||||
state.copy(visibility = if (state.visibility == View.VISIBLE) View.GONE else View.VISIBLE)
|
||||
}
|
||||
is ChangeSelectedFee -> state.copy(selectedFeeId = action.id)
|
||||
is ChangeIncludeFee -> state.copy(includeFeeIsChecked = action.isChecked)
|
||||
}
|
||||
return updateLastState(sendState.copy(feeLayoutState = result), result)
|
||||
}
|
||||
|
||||
private fun updateLastState(sendState: SendState, lastChangedState: StateType): SendState =
|
||||
sendState.copy(lastChangedStateType = lastChangedState)
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
package com.tangem.tap.features.send.redux
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.tap.common.redux.ErrorAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -12,7 +17,7 @@ interface SendScreenActionUi : SendScreenAction
|
|||
object ReleaseSendState : Action
|
||||
|
||||
data class PrepareSendScreen(
|
||||
val amount: Amount
|
||||
val amount: Amount,
|
||||
) : SendScreenAction
|
||||
|
||||
// Address or PayId
|
||||
|
|
@ -23,8 +28,7 @@ sealed class AddressPayIdActionUi : SendScreenActionUi {
|
|||
}
|
||||
|
||||
sealed class AddressPayIdVerifyAction : SendScreenAction {
|
||||
enum class FailReason {
|
||||
NONE,
|
||||
enum class Error {
|
||||
IS_NOT_PAY_ID,
|
||||
PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN,
|
||||
PAY_ID_NOT_REGISTERED,
|
||||
|
|
@ -34,12 +38,12 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
|
|||
}
|
||||
|
||||
sealed class PayIdVerification : AddressPayIdVerifyAction() {
|
||||
data class SetError(val payId: String, val reason: FailReason) : PayIdVerification()
|
||||
data class SetError(val payId: String, val error: Error) : PayIdVerification()
|
||||
data class SetPayIdWalletAddress(val payId: String, val payIdWalletAddress: String) : PayIdVerification()
|
||||
}
|
||||
|
||||
sealed class AddressVerification : AddressPayIdVerifyAction() {
|
||||
data class SetError(val address: String, val reason: FailReason) : AddressVerification()
|
||||
data class SetError(val address: String, val error: Error) : AddressVerification()
|
||||
data class SetWalletAddress(val address: String) : AddressVerification()
|
||||
}
|
||||
}
|
||||
|
|
@ -47,14 +51,68 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
|
|||
// Amount to send
|
||||
sealed class AmountActionUi : SendScreenActionUi {
|
||||
object SetMaxAmount : AmountActionUi()
|
||||
data class ChangeAmountToSend(val data: String) : AmountActionUi()
|
||||
data class CheckAmountToSend(val data: String? = null) : AmountActionUi()
|
||||
data class SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi()
|
||||
object ToggleMainCurrency : AmountActionUi()
|
||||
}
|
||||
|
||||
sealed class AmountAction : SendScreenAction {
|
||||
enum class Error {
|
||||
FEE_GREATER_THAN_AMOUNT,
|
||||
AMOUNT_WITH_FEE_GREATER_THAN_BALANCE
|
||||
}
|
||||
|
||||
sealed class AmountVerification : AmountAction() {
|
||||
data class SetAmount(val amount: BigDecimal) : AmountVerification()
|
||||
data class SetError(val amount: BigDecimal, val error: Error) : AmountVerification()
|
||||
}
|
||||
}
|
||||
|
||||
// Fee
|
||||
sealed class FeeActionUi : SendScreenActionUi {
|
||||
object ToggleFeeLayoutVisibility : FeeActionUi()
|
||||
data class ChangeSelectedFee(val id: Int) : FeeActionUi()
|
||||
class ChangeIncludeFee(val isChecked: Boolean) : FeeActionUi()
|
||||
object ToggleControlsVisibility : FeeActionUi()
|
||||
data class ChangeSelectedFee(val feeType: FeeType) : FeeActionUi()
|
||||
class ChangeIncludeFee(val isIncluded: Boolean) : FeeActionUi()
|
||||
}
|
||||
|
||||
sealed class FeeAction : SendScreenAction {
|
||||
enum class Error {
|
||||
ADDRESS_OR_AMOUNT_IS_EMPTY,
|
||||
REQUEST_FAILED
|
||||
}
|
||||
|
||||
object RequestFee : FeeAction()
|
||||
sealed class FeeCalculation : FeeAction() {
|
||||
data class SetFeeResult(val fee: List<Amount>) : FeeCalculation()
|
||||
data class SetFeeError(val error: Error) : FeeCalculation()
|
||||
}
|
||||
|
||||
data class ChangeLayoutVisibility(
|
||||
val main: Boolean? = null,
|
||||
val controls: Boolean? = null,
|
||||
val chipGroup: Boolean? = null,
|
||||
) : FeeAction()
|
||||
}
|
||||
|
||||
sealed class ReceiptAction : SendScreenAction {
|
||||
object RefreshReceipt : ReceiptAction()
|
||||
}
|
||||
|
||||
sealed class SendActionUi : SendScreenActionUi {
|
||||
object SendAmountToRecipient : SendScreenActionUi
|
||||
}
|
||||
|
||||
sealed class SendAction : SendScreenAction {
|
||||
enum class Error {
|
||||
INSUFFICIENT_BALANCE, BLOCKCHAIN_INTERNAL
|
||||
}
|
||||
|
||||
object SendSuccess : SendAction()
|
||||
|
||||
data class SendError(val sendError: Error) : SendAction(), ErrorAction {
|
||||
override val error: TapError = when (sendError) {
|
||||
Error.INSUFFICIENT_BALANCE -> TapError.InsufficientBalance
|
||||
Error.BLOCKCHAIN_INTERNAL -> TapError.BlockchainInternalError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
package com.tangem.tap.features.send.redux
|
||||
|
||||
import android.view.View
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class SendState(
|
||||
val lastChangedStateType: StateType = NoneState(),
|
||||
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
|
||||
val amountState: AmountState = AmountState(),
|
||||
val feeLayoutState: FeeLayoutState = FeeLayoutState(),
|
||||
val sendButtonIsEnabled: Boolean = false
|
||||
) : StateType
|
||||
|
||||
class NoneState : StateType
|
||||
|
||||
data class AddressPayIdState(
|
||||
val etFieldValue: String? = null,
|
||||
val normalFieldValue: String? = null,
|
||||
val truncatedFieldValue: String? = null,
|
||||
val walletAddress: String? = null,
|
||||
val error: AddressPayIdVerifyAction.FailReason? = null,
|
||||
val truncateHandler: ((String) -> String)? = null
|
||||
) : StateType {
|
||||
|
||||
fun isPayIdState(): Boolean = walletAddress != null && walletAddress != normalFieldValue
|
||||
|
||||
fun copyWalletAddress(address: String): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = address,
|
||||
normalFieldValue = address,
|
||||
truncatedFieldValue = truncated,
|
||||
walletAddress = address,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyError(address: String, error: AddressPayIdVerifyAction.FailReason): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = address,
|
||||
normalFieldValue = address,
|
||||
truncatedFieldValue = truncated,
|
||||
error = error,
|
||||
walletAddress = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyPayIdWalletAddress(payId: String, address: String): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = payId,
|
||||
normalFieldValue = payId,
|
||||
truncatedFieldValue = truncated,
|
||||
walletAddress = address,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyPaiIdError(payId: String, error: AddressPayIdVerifyAction.FailReason): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(payId) ?: payId
|
||||
return this.copy(
|
||||
etFieldValue = payId,
|
||||
normalFieldValue = payId,
|
||||
truncatedFieldValue = truncated,
|
||||
error = error,
|
||||
walletAddress = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class MainCurrencyType {
|
||||
FIAT, CRYPTO
|
||||
}
|
||||
|
||||
data class Value<T>(
|
||||
val value: T,
|
||||
val displayedValue: String
|
||||
)
|
||||
|
||||
data class AmountState(
|
||||
val etAmountFieldValue: String = BigDecimal.ZERO.toPlainString(),
|
||||
val cursorAtTheSamePosition: Boolean = true,
|
||||
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val balance: BigDecimal = BigDecimal.ZERO,
|
||||
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.main),
|
||||
val amountIsOverBalance: Boolean = false
|
||||
) : StateType
|
||||
|
||||
data class FeeLayoutState(
|
||||
val visibility: Int = View.GONE,
|
||||
val selectedFeeId: Int = R.id.chipNormal,
|
||||
val includeFeeIsChecked: Boolean = false
|
||||
) : StateType
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.commands.common.network.Result
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.PayIdManager
|
||||
import com.tangem.tap.domain.isPayIdSupported
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.*
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class AddressPayIdMiddleware {
|
||||
fun handle(data: String, appState: AppState?, dispatch: DispatchFunction) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
if (data == sendState.addressPayIdState.etFieldValue) return
|
||||
|
||||
if (PayIdManager.isPayId(data)) {
|
||||
verifyPayId(data, walletManager, dispatch)
|
||||
} else {
|
||||
val supposedAddress = extractAddressFromShareUri(data)
|
||||
dispatch(PayIdVerification.SetError(supposedAddress, Error.IS_NOT_PAY_ID))
|
||||
|
||||
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, supposedAddress)
|
||||
if (failReason == null) {
|
||||
dispatch(AddressVerification.SetWalletAddress(supposedAddress))
|
||||
} else {
|
||||
dispatch(AddressVerification.SetError(supposedAddress, failReason))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyPayId(payId: String, walletManager: WalletManager, dispatch: DispatchFunction) {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
if (!blockchain.isPayIdSupported()) {
|
||||
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val result = PayIdManager().verifyPayId(payId, blockchain)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val address = result.data.getAddress()
|
||||
if (address == null) {
|
||||
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_NOT_REGISTERED))
|
||||
return@withContext
|
||||
}
|
||||
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(walletManager.wallet, address)
|
||||
|
||||
if (failReason == null) {
|
||||
dispatch(PayIdVerification.SetPayIdWalletAddress(payId, address))
|
||||
} else {
|
||||
dispatch(AddressVerification.SetError(payId, failReason))
|
||||
}
|
||||
|
||||
}
|
||||
is Result.Failure -> {
|
||||
dispatch(PayIdVerification.SetError(payId, Error.PAY_ID_REQUEST_FAILED))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
|
||||
return if (wallet.blockchain.validateAddress(address)) {
|
||||
if (wallet.address != address) {
|
||||
null
|
||||
} else {
|
||||
Error.ADDRESS_SAME_AS_WALLET
|
||||
}
|
||||
} else {
|
||||
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: move to the blockchainSDK
|
||||
private fun extractAddressFromShareUri(shareUri: String): String {
|
||||
val sharePrefix = listOf("bitcoin:", "ethereum:", "ripple:")
|
||||
val prefixes = sharePrefix.filter { shareUri.contains(it) }
|
||||
return if (prefixes.isEmpty()) shareUri
|
||||
else shareUri.replace(prefixes[0], "")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.extensions.isGreaterThan
|
||||
import com.tangem.tap.common.extensions.isGreaterThanOrEqual
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AmountMiddleware {
|
||||
|
||||
fun handle(rawData: String?, appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
|
||||
val rawData = rawData ?: store.state.sendState.amountState.viewAmountValue
|
||||
val data = if (rawData == ".") "0.0" else rawData
|
||||
val proposedAmountValue = when {
|
||||
data.isEmpty() || data == "0" -> BigDecimal.ZERO
|
||||
else -> BigDecimal(data)
|
||||
}
|
||||
|
||||
val balanceCrypto = sendState.amount?.value ?: BigDecimal.ZERO
|
||||
val amountChecker = when (sendState.amountState.mainCurrency.value) {
|
||||
MainCurrencyType.FIAT -> FiatAmountChecker(balanceCrypto, sendState.currencyConverter)
|
||||
MainCurrencyType.CRYPTO -> CryptoAmountChecker(balanceCrypto)
|
||||
}
|
||||
|
||||
val checkResult = amountChecker.check(
|
||||
proposedAmountValue,
|
||||
sendState.feeState.getCurrentFee(),
|
||||
sendState.feeState.feeIsIncluded
|
||||
)
|
||||
if (checkResult.error == null) {
|
||||
dispatch(AmountAction.AmountVerification.SetAmount(checkResult.amount))
|
||||
} else {
|
||||
dispatch(AmountAction.AmountVerification.SetError(checkResult.amount, checkResult.error))
|
||||
}
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface AmountChecker {
|
||||
data class Result(val amount: BigDecimal, val error: AmountAction.Error? = null)
|
||||
|
||||
fun check(value: BigDecimal, feeCrypto: BigDecimal, feeIsIncluded: Boolean): Result
|
||||
}
|
||||
|
||||
abstract class BaseAmountChecker(
|
||||
protected val balanceCrypto: BigDecimal
|
||||
) : AmountChecker {
|
||||
|
||||
override fun check(value: BigDecimal, feeCrypto: BigDecimal, feeIsIncluded: Boolean): AmountChecker.Result {
|
||||
val convertedFee = convert(feeCrypto)
|
||||
val convertedBalance = convert(balanceCrypto)
|
||||
if (feeIsIncluded) {
|
||||
return if (value.isGreaterThan(convertedFee)) {
|
||||
if (convertedBalance.isGreaterThanOrEqual(value)) {
|
||||
AmountChecker.Result(value)
|
||||
} else {
|
||||
AmountChecker.Result(value, AmountAction.Error.FEE_GREATER_THAN_AMOUNT)
|
||||
}
|
||||
} else {
|
||||
AmountChecker.Result(value, AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE)
|
||||
}
|
||||
} else {
|
||||
val amountWithFee = value.plus(convertedFee)
|
||||
return if (convertedBalance.isGreaterThanOrEqual(amountWithFee)) {
|
||||
AmountChecker.Result(value)
|
||||
} else {
|
||||
AmountChecker.Result(value, AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun convert(value: BigDecimal): BigDecimal
|
||||
}
|
||||
|
||||
class CryptoAmountChecker(
|
||||
balanceCrypto: BigDecimal
|
||||
) : BaseAmountChecker(balanceCrypto) {
|
||||
override fun convert(value: BigDecimal): BigDecimal = value
|
||||
}
|
||||
|
||||
class FiatAmountChecker(
|
||||
balanceCrypto: BigDecimal,
|
||||
private val converter: CurrencyConverter
|
||||
) : BaseAmountChecker(balanceCrypto) {
|
||||
override fun convert(value: BigDecimal): BigDecimal = converter.toFiat(value)
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.DispatchFunction
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class RequestFeeMiddleware {
|
||||
|
||||
fun handle(appState: AppState?, dispatch: DispatchFunction) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = sendState.walletManager ?: return
|
||||
|
||||
if (!sendState.addressPayIdIsReady()) {
|
||||
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false, controls = true, chipGroup = true))
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
return
|
||||
}
|
||||
|
||||
val recipientAddress = sendState.addressPayIdState.recipientWalletAddress!!
|
||||
val amountState = sendState.amountState
|
||||
val cryptoSendToRecipient = amountState.amountToSendCrypto
|
||||
val recipientAmount = if (amountState.typeOfAmount == AmountType.Coin) {
|
||||
Amount(cryptoSendToRecipient, walletManager.wallet.blockchain, type = AmountType.Coin)
|
||||
} else {
|
||||
Amount(cryptoSendToRecipient, walletManager.wallet.blockchain, recipientAddress, AmountType.Token)
|
||||
}
|
||||
|
||||
val txSender = walletManager as TransactionSender
|
||||
scope.launch {
|
||||
val feeResult = txSender.getFee(recipientAmount, recipientAddress)
|
||||
withContext(Dispatchers.Main) {
|
||||
when (feeResult) {
|
||||
is Result.Success -> {
|
||||
val result = feeResult.data
|
||||
// val result = FeeMock.getFee(walletManager.wallet.blockchain)
|
||||
dispatch(FeeAction.FeeCalculation.SetFeeResult(result))
|
||||
if (result.size == 1) {
|
||||
val fee = result[0].value ?: BigDecimal.ZERO
|
||||
if (fee.isZero()) {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
|
||||
} else {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false))
|
||||
}
|
||||
} else {
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
|
||||
}
|
||||
}
|
||||
is Result.Failure -> {
|
||||
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.REQUEST_FAILED))
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false, controls = false, chipGroup = false))
|
||||
}
|
||||
}
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class FeeMock {
|
||||
companion object {
|
||||
suspend fun getFee(blockchain: Blockchain): List<Amount> {
|
||||
return feeStandard(blockchain)
|
||||
// return feeSingle(blockchain)
|
||||
// return feeStellar(blockchain)
|
||||
// return feeZero(blockchain)
|
||||
}
|
||||
|
||||
suspend fun feeStellar(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0001.toBigDecimal(), blockchain))
|
||||
|
||||
suspend fun feeZero(blockchain: Blockchain): List<Amount> = listOf(Amount(BigDecimal.ZERO, blockchain))
|
||||
|
||||
suspend fun feeStandard(blockchain: Blockchain): List<Amount> = listOf(
|
||||
Amount(0.001500.toBigDecimal(), blockchain),
|
||||
Amount(0.0030.toBigDecimal(), blockchain),
|
||||
Amount(0.0045001.toBigDecimal(), blockchain)
|
||||
)
|
||||
|
||||
suspend fun feeStandardBig(blockchain: Blockchain): List<Amount> = listOf(
|
||||
Amount(1.76.toBigDecimal(), blockchain),
|
||||
Amount(2.30.toBigDecimal(), blockchain),
|
||||
Amount(3.45.toBigDecimal(), blockchain)
|
||||
)
|
||||
|
||||
suspend fun feeSingle(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0015.toBigDecimal(), blockchain))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.ChangeAddressOrPayId
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
|
||||
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
|
||||
import com.tangem.tap.features.send.redux.SendAction
|
||||
import com.tangem.tap.features.send.redux.SendActionUi
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
is ChangeAddressOrPayId -> AddressPayIdMiddleware().handle(action.data, appState(), dispatch)
|
||||
is CheckAmountToSend -> AmountMiddleware().handle(action.data, appState(), dispatch)
|
||||
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
|
||||
is SendActionUi.SendAmountToRecipient -> verifyAndSendTransaction(appState(), dispatch)
|
||||
}
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyAndSendTransaction(appState: AppState?, dispatch: (Action) -> Unit) {
|
||||
val sendState = appState?.sendState ?: return
|
||||
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
|
||||
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val recipientAddress = sendState.addressPayIdState.recipientWalletAddress!!
|
||||
|
||||
val feeAmount = Amount(sendState.feeState.getCurrentFee(), blockchain)
|
||||
val amountToSend = Amount(sendState.amountState.amountToSendCrypto, blockchain, recipientAddress)
|
||||
|
||||
scope.launch {
|
||||
when (val sendResult = tangemSdkManager.send(walletManager, recipientAddress, amountToSend, feeAmount)) {
|
||||
is CompletionResult.Success -> dispatch(SendAction.SendSuccess)
|
||||
is CompletionResult.Failure -> {
|
||||
when (sendResult.error.code) {
|
||||
1021 -> dispatch(SendAction.SendError(SendAction.Error.INSUFFICIENT_BALANCE))
|
||||
1001, 2000 -> dispatch(SendAction.SendError(SendAction.Error.BLOCKCHAIN_INTERNAL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.states.AddressPayIdState
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddressPayIdReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is AddressPayIdActionUi -> handleUiAction(action, sendState, sendState.addressPayIdState)
|
||||
is AddressPayIdVerifyAction -> handleAction(action, sendState, sendState.addressPayIdState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: AddressPayIdActionUi, sendState: SendState, state: AddressPayIdState): SendState {
|
||||
val result = when (action) {
|
||||
is AddressPayIdActionUi.ChangeAddressOrPayId -> state
|
||||
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
|
||||
is AddressPayIdActionUi.TruncateOrRestore -> {
|
||||
if (action.truncate) state.copy(etFieldValue = state.truncatedFieldValue)
|
||||
else state.copy(etFieldValue = state.normalFieldValue)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(addressPayIdState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: AddressPayIdVerifyAction, sendState: SendState, state: AddressPayIdState): SendState {
|
||||
val result = when (action) {
|
||||
is PayIdVerification.SetPayIdWalletAddress -> state.copyPayIdWalletAddress(action.payId, action.payIdWalletAddress)
|
||||
is PayIdVerification.SetError -> state.copyPayIdError(action.payId, action.error)
|
||||
is AddressVerification.SetWalletAddress -> state.copyWalletAddress(action.address)
|
||||
is AddressVerification.SetError -> state.copyError(action.address, action.error)
|
||||
}
|
||||
return updateLastState(sendState.copy(addressPayIdState = result), result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.tap.common.extensions.isNegative
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.*
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.states.AmountState
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.features.send.redux.states.Value
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AmountReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is AmountActionUi -> handleUiAction(action, sendState, sendState.amountState)
|
||||
is AmountAction -> handleAction(action, sendState, sendState.amountState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
|
||||
val converter = sendState.currencyConverter
|
||||
val result = when (action) {
|
||||
is ToggleMainCurrency -> {
|
||||
val type = if (state.mainCurrency.value == MainCurrencyType.FIAT) MainCurrencyType.CRYPTO
|
||||
else MainCurrencyType.FIAT
|
||||
|
||||
return handleUiAction(SetMainCurrency(type), sendState, state)
|
||||
}
|
||||
is SetMainCurrency -> {
|
||||
when (action.mainCurrency) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val fiatToSend = if (state.amountToSendCrypto.isZero()) BigDecimal.ZERO
|
||||
else converter.toFiat(state.amountToSendCrypto)
|
||||
state.copy(
|
||||
viewAmountValue = fiatToSend.stripZeroPlainString(),
|
||||
viewBalanceValue = converter.toFiat(state.balanceCrypto).stripZeroPlainString(),
|
||||
mainCurrency = Value(MainCurrencyType.FIAT, TapCurrency.main),
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
val mainCurrency = Value(MainCurrencyType.CRYPTO, sendState.amount?.currencySymbol ?: "null")
|
||||
state.copy(
|
||||
viewAmountValue = state.amountToSendCrypto.stripZeroPlainString(),
|
||||
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
|
||||
mainCurrency = mainCurrency,
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is SetMaxAmount -> {
|
||||
val maxAmount = if (sendState.feeState.feeIsIncluded) {
|
||||
state.balanceCrypto
|
||||
} else {
|
||||
val balanceExtractFee = state.balanceCrypto.minus(sendState.feeState.getCurrentFee())
|
||||
if (balanceExtractFee.isNegative()) BigDecimal.ZERO
|
||||
else balanceExtractFee
|
||||
}
|
||||
|
||||
val etFieldValue = if (state.mainCurrency.value == MainCurrencyType.CRYPTO) maxAmount
|
||||
else converter.toFiat(maxAmount)
|
||||
state.copy(
|
||||
viewAmountValue = etFieldValue.stripZeroPlainString(),
|
||||
amountToSendCrypto = maxAmount,
|
||||
cursorAtTheSamePosition = false
|
||||
)
|
||||
}
|
||||
is CheckAmountToSend -> return sendState
|
||||
}
|
||||
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState {
|
||||
val decimals = sendState.amount?.decimals ?: return sendState
|
||||
|
||||
val result = when (action) {
|
||||
is AmountAction.AmountVerification.SetAmount -> {
|
||||
setAmount(sendState.currencyConverter, decimals, action.amount, state)
|
||||
}
|
||||
is AmountAction.AmountVerification.SetError -> {
|
||||
setAmount(sendState.currencyConverter, decimals, action.amount, state).copy(error = action.error)
|
||||
}
|
||||
}
|
||||
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
|
||||
}
|
||||
|
||||
private fun setAmount(converter: CurrencyConverter, decimals: Int, amount: BigDecimal, state: AmountState): AmountState {
|
||||
return when (state.mainCurrency.value) {
|
||||
MainCurrencyType.FIAT -> {
|
||||
val amountCrypto = converter.toCrypto(amount, decimals).stripTrailingZeros()
|
||||
state.copy(
|
||||
viewAmountValue = amount.stripZeroPlainString(),
|
||||
amountToSendCrypto = amountCrypto,
|
||||
cursorAtTheSamePosition = true,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> {
|
||||
state.copy(
|
||||
viewAmountValue = amount.stripZeroPlainString(),
|
||||
amountToSendCrypto = amount,
|
||||
cursorAtTheSamePosition = true,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.extensions.isAboveZero
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.states.FeeState
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.features.send.redux.states.Value
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeeReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
|
||||
is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState)
|
||||
is FeeAction -> handleAction(action, sendState, sendState.feeState)
|
||||
else -> sendState
|
||||
}
|
||||
|
||||
private fun handleUiAction(action: FeeActionUi, sendState: SendState, state: FeeState): SendState {
|
||||
val result = when (action) {
|
||||
is FeeActionUi.ToggleControlsVisibility -> state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible)
|
||||
is FeeActionUi.ChangeSelectedFee -> {
|
||||
val currentFee = createValueOfFeeAmount(action.feeType, state.feeList)
|
||||
state.copy(
|
||||
selectedFeeType = action.feeType,
|
||||
currentFee = currentFee
|
||||
)
|
||||
|
||||
}
|
||||
is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded)
|
||||
}
|
||||
return updateLastState(sendState.copy(feeState = result), result)
|
||||
}
|
||||
|
||||
private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState {
|
||||
val result = when (action) {
|
||||
is FeeAction.RequestFee -> {
|
||||
state.copy(error = null)
|
||||
}
|
||||
is FeeAction.ChangeLayoutVisibility -> {
|
||||
fun getVisibility(current: Boolean, proposed: Boolean?): Boolean = proposed ?: current
|
||||
state.copy(
|
||||
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
|
||||
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
|
||||
feeChipGroupIsVisible = getVisibility(state.mainLayoutIsVisible, action.chipGroup)
|
||||
)
|
||||
}
|
||||
is FeeAction.FeeCalculation.SetFeeResult -> {
|
||||
val fees = action.fee
|
||||
if (fees.size == 1) {
|
||||
val feeType = FeeType.SINGLE
|
||||
val currentFee = createValueOfFeeAmount(feeType, fees)
|
||||
|
||||
state.copy(
|
||||
selectedFeeType = feeType,
|
||||
feeList = fees,
|
||||
currentFee = currentFee,
|
||||
error = null
|
||||
)
|
||||
} else {
|
||||
val feeType = getCurrentFeeType(state)
|
||||
val currentFee = createValueOfFeeAmount(feeType, fees)
|
||||
|
||||
state.copy(
|
||||
selectedFeeType = feeType,
|
||||
feeList = fees,
|
||||
currentFee = currentFee,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
is FeeAction.FeeCalculation.SetFeeError -> {
|
||||
state.copy(
|
||||
feeList = null,
|
||||
currentFee = null,
|
||||
error = action.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return updateLastState(sendState.copy(feeState = result), result)
|
||||
}
|
||||
|
||||
private fun createValueOfFeeAmount(feeType: FeeType, list: List<Amount>?): Value<Amount>? {
|
||||
if (list == null || list.isEmpty()) return null
|
||||
val feeAmount = if (list.size == 1) {
|
||||
if (!list[0].isAboveZero()) return null
|
||||
|
||||
list[0]
|
||||
} else {
|
||||
when (feeType) {
|
||||
FeeType.SINGLE -> list[1]
|
||||
FeeType.LOW -> list[0]
|
||||
FeeType.NORMAL -> list[1]
|
||||
FeeType.PRIORITY -> list[2]
|
||||
}
|
||||
}
|
||||
|
||||
return Value(feeAmount, feeAmount.value?.stripZeroPlainString() ?: "")
|
||||
}
|
||||
|
||||
private fun getCurrentFeeType(state: FeeState): FeeType {
|
||||
return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
|
||||
import com.tangem.tap.features.send.redux.SendScreenAction
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ReceiptReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
return when (action) {
|
||||
is RefreshReceipt -> handleRefresh(action, sendState, sendState.receiptState)
|
||||
else -> sendState
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun handleRefresh(action: RefreshReceipt, sendState: SendState, state: ReceiptState): SendState {
|
||||
val wallet = sendState.walletManager?.wallet ?: return sendState
|
||||
|
||||
val converter = sendState.currencyConverter
|
||||
val amountState = sendState.amountState
|
||||
val feeState = sendState.feeState
|
||||
|
||||
val layoutType = determineLayoutType(amountState.mainCurrency.value, amountState.typeOfAmount)
|
||||
val symbols = determineSymbols(wallet)
|
||||
val result = state.copy(
|
||||
visibleTypeOfReceipt = layoutType,
|
||||
mainCurrencyType = sendState.amountState.mainCurrency,
|
||||
mainLayoutIsVisible = sendState.isReadyToSend(),
|
||||
fiat = createFiatType(converter, amountState, feeState, symbols),
|
||||
crypto = createCryptoType(converter, amountState, feeState, symbols),
|
||||
tokenFiat = createTokenFiatType(converter, amountState, feeState, symbols),
|
||||
tokenCrypto = createTokenCryptoType(converter, amountState, feeState, symbols)
|
||||
)
|
||||
return updateLastState(sendState.copy(receiptState = result), result)
|
||||
}
|
||||
|
||||
private fun createFiatType(
|
||||
converter: CurrencyConverter,
|
||||
amountState: AmountState,
|
||||
feeState: FeeState,
|
||||
symbols: ReceiptSymbols
|
||||
): ReceiptFiat {
|
||||
val feeCrypto = feeState.getCurrentFee()
|
||||
val feeFiat = converter.toFiat(feeCrypto)
|
||||
|
||||
return if (feeState.feeIsIncluded) {
|
||||
val amountFiat = converter.toFiat(amountState.amountToSendCrypto.minus(feeCrypto))
|
||||
val totalFiat = converter.toFiat(amountState.amountToSendCrypto)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = amountState.amountToSendCrypto.stripTrailingZeros(),
|
||||
symbols = symbols
|
||||
)
|
||||
} else {
|
||||
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
val amountFiat = converter.toFiat(amountState.amountToSendCrypto)
|
||||
val totalFiat = converter.toFiat(totalAmountCrypto)
|
||||
ReceiptFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = totalFiat,
|
||||
willSentCrypto = totalAmountCrypto.stripTrailingZeros(),
|
||||
symbols = symbols
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createCryptoType(
|
||||
converter: CurrencyConverter,
|
||||
amountState: AmountState,
|
||||
feeState: FeeState,
|
||||
symbols: ReceiptSymbols
|
||||
): ReceiptCrypto {
|
||||
val feeCrypto = feeState.getCurrentFee()
|
||||
|
||||
if (feeState.feeIsIncluded) {
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripTrailingZeros(),
|
||||
feeCrypto = feeCrypto.stripTrailingZeros(),
|
||||
totalCrypto = amountState.amountToSendCrypto.stripTrailingZeros(),
|
||||
willSentFiat = converter.toFiat(amountState.amountToSendCrypto),
|
||||
symbols = symbols
|
||||
)
|
||||
} else {
|
||||
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
|
||||
return ReceiptCrypto(
|
||||
amountCrypto = amountState.amountToSendCrypto.stripTrailingZeros(),
|
||||
feeCrypto = feeCrypto.stripTrailingZeros(),
|
||||
totalCrypto = totalCrypto.stripTrailingZeros(),
|
||||
willSentFiat = converter.toFiat(totalCrypto),
|
||||
symbols = symbols
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createTokenFiatType(
|
||||
converter: CurrencyConverter,
|
||||
amountState: AmountState,
|
||||
feeState: FeeState,
|
||||
symbols: ReceiptSymbols
|
||||
): ReceiptTokenFiat {
|
||||
val feeCrypto = feeState.getCurrentFee()
|
||||
val amountFiat = converter.toFiat(amountState.amountToSendCrypto)
|
||||
val feeFiat = converter.toFiat(feeCrypto)
|
||||
return ReceiptTokenFiat(
|
||||
amountFiat = amountFiat,
|
||||
feeFiat = feeFiat,
|
||||
totalFiat = amountFiat.plus(feeFiat),
|
||||
willSentTokenCrypto = amountState.amountToSendCrypto.stripTrailingZeros(),
|
||||
willSentFeeCrypto = feeCrypto.stripTrailingZeros(),
|
||||
symbols = symbols
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTokenCryptoType(
|
||||
converter: CurrencyConverter,
|
||||
amountState: AmountState,
|
||||
feeState: FeeState,
|
||||
symbols: ReceiptSymbols
|
||||
): ReceiptTokenCrypto {
|
||||
val feeCrypto = feeState.getCurrentFee()
|
||||
val totalFiat = converter.toFiat(amountState.amountToSendCrypto).plus(converter.toFiat(feeCrypto))
|
||||
return ReceiptTokenCrypto(
|
||||
amountToken = amountState.amountToSendCrypto.stripTrailingZeros(),
|
||||
feeCrypto = feeCrypto.stripTrailingZeros(),
|
||||
totalFiat = totalFiat.stripTrailingZeros(),
|
||||
symbols = symbols
|
||||
)
|
||||
}
|
||||
|
||||
private fun determineSymbols(wallet: Wallet): ReceiptSymbols {
|
||||
return ReceiptSymbols(
|
||||
fiat = TapCurrency.main,
|
||||
crypto = wallet.blockchain.currency,
|
||||
token = wallet.amounts[AmountType.Token]?.currencySymbol
|
||||
)
|
||||
}
|
||||
|
||||
private fun determineLayoutType(mainCurrencyType: MainCurrencyType, amountType: AmountType): ReceiptLayoutType {
|
||||
return when (mainCurrencyType) {
|
||||
MainCurrencyType.FIAT -> when (amountType) {
|
||||
AmountType.Coin -> ReceiptLayoutType.FIAT
|
||||
AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
|
||||
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN
|
||||
}
|
||||
MainCurrencyType.CRYPTO -> when (amountType) {
|
||||
AmountType.Coin -> ReceiptLayoutType.CRYPTO
|
||||
AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
|
||||
AmountType.Reserve -> ReceiptLayoutType.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.StateType
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface SendInternalReducer {
|
||||
fun handle(action: SendScreenAction, sendState: SendState): SendState
|
||||
}
|
||||
|
||||
class SendReducer {
|
||||
companion object {
|
||||
fun reduce(incomingAction: Action, sendState: SendState): SendState {
|
||||
if (incomingAction is ReleaseSendState) return SendState()
|
||||
val action = incomingAction as? SendScreenAction ?: return sendState
|
||||
|
||||
val reducer: SendInternalReducer = when (action) {
|
||||
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
|
||||
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
|
||||
is AmountActionUi, is AmountAction -> AmountReducer()
|
||||
is FeeActionUi, is FeeAction -> FeeReducer()
|
||||
is ReceiptAction -> ReceiptReducer()
|
||||
else -> EmptyReducer()
|
||||
}
|
||||
|
||||
|
||||
val newState = reducer.handle(action, sendState).copy(sendButtonIsEnabled = sendState.isReadyToSend())
|
||||
Timber.i("${newState.lastChangedStateType}.")
|
||||
|
||||
return newState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = sendState
|
||||
}
|
||||
|
||||
private class PrepareSendScreenStatesReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
val amount = (action as PrepareSendScreen).amount
|
||||
val walletManager = store.state.globalState.scanNoteResponse!!.walletManager!!
|
||||
return sendState.copy(
|
||||
amount = amount,
|
||||
walletManager = walletManager,
|
||||
currencyConverter = createCurrencyConverter(walletManager),
|
||||
amountState = sendState.amountState.copy(balanceCrypto = amount.value ?: BigDecimal.ZERO)
|
||||
)
|
||||
}
|
||||
|
||||
private fun createCurrencyConverter(walletManager: WalletManager): CurrencyConverter {
|
||||
val rate = store.state.globalState.fiatRates.getRateForCryptoCurrency(walletManager.wallet.blockchain.currency)
|
||||
return if (rate == null) CurrencyConverter(BigDecimal.ONE) else CurrencyConverter(rate)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateLastState(sendState: SendState, lastChangedState: StateType): SendState =
|
||||
sendState.copy(lastChangedStateType = lastChangedState)
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddressPayIdState(
|
||||
val etFieldValue: String? = null,
|
||||
val normalFieldValue: String? = null,
|
||||
val truncatedFieldValue: String? = null,
|
||||
val recipientWalletAddress: String? = null,
|
||||
val error: AddressPayIdVerifyAction.Error? = null,
|
||||
val truncateHandler: ((String) -> String)? = null
|
||||
) : StateType {
|
||||
|
||||
fun isReady(): Boolean = error == null && recipientWalletAddress?.isNotEmpty() ?: false
|
||||
|
||||
fun isPayIdState(): Boolean = recipientWalletAddress != null && recipientWalletAddress != normalFieldValue
|
||||
|
||||
fun copyWalletAddress(address: String): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = address,
|
||||
normalFieldValue = address,
|
||||
truncatedFieldValue = truncated,
|
||||
recipientWalletAddress = address,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyError(address: String, error: AddressPayIdVerifyAction.Error): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = address,
|
||||
normalFieldValue = address,
|
||||
truncatedFieldValue = truncated,
|
||||
error = error,
|
||||
recipientWalletAddress = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyPayIdWalletAddress(payId: String, address: String): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(address) ?: address
|
||||
return this.copy(
|
||||
etFieldValue = payId,
|
||||
normalFieldValue = payId,
|
||||
truncatedFieldValue = truncated,
|
||||
recipientWalletAddress = address,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
|
||||
fun copyPayIdError(payId: String, error: AddressPayIdVerifyAction.Error): AddressPayIdState {
|
||||
val truncated = truncateHandler?.invoke(payId) ?: payId
|
||||
return this.copy(
|
||||
etFieldValue = payId,
|
||||
normalFieldValue = payId,
|
||||
truncatedFieldValue = truncated,
|
||||
error = error,
|
||||
recipientWalletAddress = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class FeeType {
|
||||
SINGLE, LOW, NORMAL, PRIORITY
|
||||
}
|
||||
|
||||
data class FeeState(
|
||||
val selectedFeeType: FeeType = FeeType.NORMAL,
|
||||
val feeList: List<Amount>? = null,
|
||||
val currentFee: Value<Amount>? = null,
|
||||
val feeIsIncluded: Boolean = false,
|
||||
val mainLayoutIsVisible: Boolean = false,
|
||||
val controlsLayoutIsVisible: Boolean = true,
|
||||
val feeChipGroupIsVisible: Boolean = true,
|
||||
val error: FeeAction.Error? = null
|
||||
) : StateType {
|
||||
fun isReady(): Boolean = error == null && currentFee != null
|
||||
|
||||
fun getCurrentFee(): BigDecimal = currentFee?.value?.value ?: BigDecimal.ZERO
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
||||
// Shows only one type of the layout
|
||||
enum class ReceiptLayoutType {
|
||||
UNKNOWN, FIAT, CRYPTO, TOKEN_FIAT, TOKEN_CRYPTO
|
||||
}
|
||||
|
||||
data class ReceiptState(
|
||||
val visibleTypeOfReceipt: ReceiptLayoutType? = null,
|
||||
val fiat: ReceiptFiat? = null,
|
||||
val crypto: ReceiptCrypto? = null,
|
||||
val tokenFiat: ReceiptTokenFiat? = null,
|
||||
val tokenCrypto: ReceiptTokenCrypto? = null,
|
||||
val mainCurrencyType: Value<MainCurrencyType>? = null,
|
||||
val mainLayoutIsVisible: Boolean = false,
|
||||
) : StateType
|
||||
|
||||
data class ReceiptSymbols(
|
||||
val fiat: String,
|
||||
val crypto: String,
|
||||
val token: String? = null
|
||||
)
|
||||
|
||||
data class ReceiptFiat(
|
||||
val amountFiat: BigDecimal,
|
||||
val feeFiat: BigDecimal,
|
||||
val totalFiat: BigDecimal,
|
||||
val willSentCrypto: BigDecimal,
|
||||
val symbols: ReceiptSymbols
|
||||
)
|
||||
|
||||
data class ReceiptCrypto(
|
||||
val amountCrypto: BigDecimal,
|
||||
val feeCrypto: BigDecimal,
|
||||
val totalCrypto: BigDecimal,
|
||||
val willSentFiat: BigDecimal,
|
||||
val symbols: ReceiptSymbols
|
||||
)
|
||||
|
||||
data class ReceiptTokenCrypto(
|
||||
val amountToken: BigDecimal,
|
||||
val feeCrypto: BigDecimal,
|
||||
val totalFiat: BigDecimal,
|
||||
val symbols: ReceiptSymbols
|
||||
)
|
||||
|
||||
data class ReceiptTokenFiat(
|
||||
val amountFiat: BigDecimal,
|
||||
val feeFiat: BigDecimal,
|
||||
val totalFiat: BigDecimal,
|
||||
val willSentTokenCrypto: BigDecimal,
|
||||
val willSentFeeCrypto: BigDecimal,
|
||||
val symbols: ReceiptSymbols
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.tap.features.send.redux.states
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.CurrencyConverter
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.StateType
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class SendState(
|
||||
val amount: Amount? = null,
|
||||
val walletManager: WalletManager? = null,
|
||||
val currencyConverter: CurrencyConverter = CurrencyConverter(BigDecimal.ONE),
|
||||
val lastChangedStateType: StateType = NoneState(),
|
||||
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
|
||||
val amountState: AmountState = AmountState(),
|
||||
val feeState: FeeState = FeeState(),
|
||||
val receiptState: ReceiptState = ReceiptState(),
|
||||
val sendButtonIsEnabled: Boolean = false,
|
||||
) : StateType {
|
||||
|
||||
fun isReadyToSend(): Boolean {
|
||||
val sendState = store.state.sendState
|
||||
return addressPayIdIsReady() && sendState.amountState.isReady() && sendState.feeState.isReady()
|
||||
}
|
||||
|
||||
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
|
||||
}
|
||||
|
||||
class NoneState : StateType
|
||||
|
||||
data class AmountState(
|
||||
val viewAmountValue: String = BigDecimal.ZERO.toPlainString(),
|
||||
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
|
||||
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.main),
|
||||
val typeOfAmount: AmountType = AmountType.Coin,
|
||||
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val cursorAtTheSamePosition: Boolean = true,
|
||||
val error: AmountAction.Error? = null
|
||||
) : StateType {
|
||||
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
|
||||
}
|
||||
|
||||
enum class MainCurrencyType {
|
||||
FIAT, CRYPTO
|
||||
}
|
||||
|
||||
data class Value<T>(
|
||||
val value: T,
|
||||
val displayedValue: String
|
||||
)
|
||||
|
|
@ -4,30 +4,38 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
|
||||
import com.tangem.tap.common.KeyboardObserver
|
||||
import com.tangem.tap.common.entities.TapCurrency
|
||||
import com.tangem.tap.common.extensions.getFromClipboard
|
||||
import com.tangem.tap.common.extensions.setOnImeActionListener
|
||||
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
|
||||
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
|
||||
import com.tangem.tap.common.text.truncateMiddleWith
|
||||
import com.tangem.tap.features.send.BaseStoreFragment
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
|
||||
import com.tangem.tap.features.send.redux.AmountActionUi.*
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.FeeActionUi.*
|
||||
import com.tangem.tap.features.send.redux.MainCurrencyType
|
||||
import com.tangem.tap.features.send.redux.ReceiptAction
|
||||
import com.tangem.tap.features.send.redux.ReleaseSendState
|
||||
import com.tangem.tap.features.send.redux.SendActionUi
|
||||
import com.tangem.tap.features.send.redux.states.FeeType
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.android.synthetic.main.btn_paste.*
|
||||
import kotlinx.android.synthetic.main.btn_qr_code.*
|
||||
import kotlinx.android.synthetic.main.fragment_send.*
|
||||
import kotlinx.android.synthetic.main.layout_send_address_payid.*
|
||||
import kotlinx.android.synthetic.main.layout_send_amount.*
|
||||
import kotlinx.android.synthetic.main.layout_send_network_fee.*
|
||||
import kotlinx.android.synthetic.main.layout_send_fee.*
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -46,6 +54,10 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
setupAddressOrPayIdLayout()
|
||||
setupAmountLayout()
|
||||
setupFeeLayout()
|
||||
|
||||
btnSend.setOnClickListener {
|
||||
store.dispatch(SendActionUi.SendAmountToRecipient)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAddressOrPayIdLayout() {
|
||||
|
|
@ -54,15 +66,19 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
etAddressOrPayId.setOnFocusChangeListener { v, hasFocus ->
|
||||
store.dispatch(TruncateOrRestore(!hasFocus))
|
||||
}
|
||||
etAddressOrPayId.inputedTextAsFlow()
|
||||
etAddressOrPayId.inputtedTextAsFlow()
|
||||
.debounce(400)
|
||||
.filter { store.state.sendState.addressPayIdState.etFieldValue != it }
|
||||
.onEach { store.dispatch(ChangeAddressOrPayId(it)) }
|
||||
.onEach {
|
||||
store.dispatch(ChangeAddressOrPayId(it))
|
||||
store.dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
.launchIn(mainScope)
|
||||
|
||||
imvPaste.setOnClickListener {
|
||||
store.dispatch(ChangeAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: ""))
|
||||
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
|
||||
store.dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
imvQrCode.setOnClickListener {
|
||||
startActivityForResult(
|
||||
|
|
@ -78,13 +94,22 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
|
||||
store.dispatch(ChangeAddressOrPayId(scannedCode))
|
||||
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
|
||||
view?.postDelayed(200) { store.dispatch(FeeAction.RequestFee) }
|
||||
}
|
||||
|
||||
private fun setupAmountLayout() {
|
||||
store.dispatch(SetMainCurrency(restoreMainCurrency()))
|
||||
tvAmountCurrency.setOnClickListener { store.dispatch(ToggleMainCurrency) }
|
||||
tvAmountCurrency.setOnClickListener {
|
||||
store.dispatch(ToggleMainCurrency)
|
||||
store.dispatch(ReceiptAction.RefreshReceipt)
|
||||
}
|
||||
|
||||
val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) { store.dispatch(SetMaxAmount) }
|
||||
val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) {
|
||||
etAmountToSend.clearFocus()
|
||||
etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() }
|
||||
store.dispatch(SetMaxAmount)
|
||||
store.dispatch(CheckAmountToSend())
|
||||
}
|
||||
var snackbarControlledByChangingFocus = false
|
||||
keyboardObserver = KeyboardObserver(requireActivity())
|
||||
keyboardObserver.registerListener { isShow ->
|
||||
|
|
@ -103,7 +128,6 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
etAmountToSend.postDelayed(200) {
|
||||
maxAmountSnackbar.show()
|
||||
snackbarControlledByChangingFocus = false
|
||||
|
||||
}
|
||||
} else {
|
||||
etAmountToSend.postDelayed(350) {
|
||||
|
|
@ -120,33 +144,44 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0")
|
||||
}
|
||||
|
||||
etAmountToSend.inputedTextAsFlow()
|
||||
etAmountToSend.inputtedTextAsFlow()
|
||||
.debounce(400)
|
||||
.filter { store.state.sendState.amountState.etAmountFieldValue != it }
|
||||
.onEach { store.dispatch(ChangeAmountToSend(it)) }
|
||||
.filter { store.state.sendState.amountState.viewAmountValue != it && it.isNotEmpty() }
|
||||
.onEach { store.dispatch(CheckAmountToSend(it)) }
|
||||
.launchIn(mainScope)
|
||||
|
||||
etAmountToSend.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
|
||||
it.hideSoftKeyboard()
|
||||
it.clearFocus()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFeeLayout() {
|
||||
flExpandCollapse.setOnClickListener {
|
||||
store.dispatch(ToggleFeeLayoutVisibility)
|
||||
store.dispatch(ToggleControlsVisibility)
|
||||
}
|
||||
chipGroup.setOnCheckedChangeListener { group, checkedId ->
|
||||
store.dispatch(ChangeSelectedFee(checkedId))
|
||||
if (checkedId == -1) return@setOnCheckedChangeListener
|
||||
|
||||
store.dispatch(ChangeSelectedFee(FeeUiHelper.idToFee(checkedId)))
|
||||
store.dispatch(CheckAmountToSend())
|
||||
}
|
||||
swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
|
||||
store.dispatch(ChangeIncludeFee(isChecked))
|
||||
store.dispatch(CheckAmountToSend())
|
||||
}
|
||||
}
|
||||
|
||||
override fun subscribeToStore() {
|
||||
store.subscribe(sendSubscriber) { appState ->
|
||||
appState.skipRepeats { oldState, newState -> oldState == newState }.select { it.sendState }
|
||||
appState.skipRepeats { oldState, newState ->
|
||||
oldState.sendState == newState.sendState
|
||||
}.select { it.sendState }
|
||||
}
|
||||
storeSubscribersList.add(sendSubscriber)
|
||||
}
|
||||
|
||||
fun restoreMainCurrency(): MainCurrencyType {
|
||||
private fun restoreMainCurrency(): MainCurrencyType {
|
||||
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
|
||||
val mainCurrency = sp.getString("mainCurrency", TapCurrency.main)
|
||||
val foundType = MainCurrencyType.values()
|
||||
|
|
@ -167,10 +202,32 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun EditText.inputedTextAsFlow(): Flow<String> = callbackFlow {
|
||||
fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
|
||||
val watcher = addTextChangedListener { editable -> offer(editable?.toString() ?: "") }
|
||||
awaitClose { removeTextChangedListener(watcher) }
|
||||
}
|
||||
|
||||
class FeeUiHelper {
|
||||
companion object {
|
||||
fun feeToId(fee: FeeType): Int {
|
||||
return when (fee) {
|
||||
FeeType.SINGLE -> 0
|
||||
FeeType.LOW -> R.id.chipLow
|
||||
FeeType.NORMAL -> R.id.chipNormal
|
||||
FeeType.PRIORITY -> R.id.chipPriority
|
||||
}
|
||||
}
|
||||
|
||||
fun idToFee(id: Int): FeeType {
|
||||
return when (id) {
|
||||
R.id.chipLow -> FeeType.LOW
|
||||
R.id.chipNormal -> FeeType.NORMAL
|
||||
R.id.chipPriority -> FeeType.PRIORITY
|
||||
else -> FeeType.NORMAL
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.tap.features.send.ui.stateSubscribers
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.tangem.tap.features.send.BaseStoreFragment
|
||||
import org.rekotlin.StateType
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
@ -8,10 +8,10 @@ import java.lang.ref.WeakReference
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
abstract class FragmentStateSubscriber<S : StateType>(fragment: Fragment) : StoreSubscriber<S> {
|
||||
private val weakFragment: WeakReference<Fragment> = WeakReference(fragment)
|
||||
abstract class FragmentStateSubscriber<S : StateType>(fragment: BaseStoreFragment) : StoreSubscriber<S> {
|
||||
private val weakFragment: WeakReference<BaseStoreFragment> = WeakReference(fragment)
|
||||
|
||||
abstract fun updateWithNewState(fg: Fragment, state: S)
|
||||
abstract fun updateWithNewState(fg: BaseStoreFragment, state: S)
|
||||
|
||||
override fun newState(state: S) {
|
||||
val fg = weakFragment.get() ?: return
|
||||
|
|
|
|||
|
|
@ -1,49 +1,58 @@
|
|||
package com.tangem.tap.features.send.ui.stateSubscribers
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.core.text.bold
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.enableError
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.extensions.update
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdState
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.FailReason
|
||||
import com.tangem.tap.features.send.redux.AmountState
|
||||
import com.tangem.tap.features.send.redux.FeeLayoutState
|
||||
import com.tangem.tap.features.send.redux.SendState
|
||||
import com.tangem.tap.features.send.BaseStoreFragment
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.features.send.redux.FeeAction
|
||||
import com.tangem.tap.features.send.redux.states.*
|
||||
import com.tangem.tap.features.send.ui.FeeUiHelper
|
||||
import com.tangem.tap.features.send.ui.SendFragment
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.android.synthetic.main.btn_expand_collapse.*
|
||||
import kotlinx.android.synthetic.main.fragment_send.*
|
||||
import kotlinx.android.synthetic.main.fragment_send.clReceiptContainer
|
||||
import kotlinx.android.synthetic.main.layout_receipt_total.*
|
||||
import kotlinx.android.synthetic.main.layout_receipt_total.view.*
|
||||
import kotlinx.android.synthetic.main.layout_send_address_payid.*
|
||||
import kotlinx.android.synthetic.main.layout_send_amount.*
|
||||
import kotlinx.android.synthetic.main.layout_send_network_fee.*
|
||||
import kotlinx.android.synthetic.main.layout_send_fee.*
|
||||
import kotlinx.android.synthetic.main.layout_send_receipt.*
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendState>(fragment) {
|
||||
class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber<SendState>(fragment) {
|
||||
|
||||
override fun updateWithNewState(fg: Fragment, state: SendState) {
|
||||
override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) {
|
||||
when (state.lastChangedStateType) {
|
||||
is FeeLayoutState -> handleFeeLayoutState(fg, state.feeLayoutState)
|
||||
is FeeState -> handleFeeState(fg, state.feeState)
|
||||
is AddressPayIdState -> handleAddressPayIdState(fg, state.addressPayIdState)
|
||||
is AmountState -> handleAmountState(fg, state.amountState)
|
||||
is ReceiptState -> handleReceiptState(fg, state.receiptState)
|
||||
}
|
||||
|
||||
fg.btnSend.isEnabled = state.sendButtonIsEnabled
|
||||
}
|
||||
|
||||
private fun handleAddressPayIdState(fg: Fragment, state: AddressPayIdState) {
|
||||
fun parseError(context: Context, error: FailReason?): String? {
|
||||
private fun handleAddressPayIdState(fg: BaseStoreFragment, state: AddressPayIdState) {
|
||||
fun parseError(context: Context, error: Error?): String? {
|
||||
val resId = when (error) {
|
||||
FailReason.IS_NOT_PAY_ID -> R.string.error_payid_verification_failed
|
||||
FailReason.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.error_payid_unsupported_by_blockchain
|
||||
FailReason.PAY_ID_NOT_REGISTERED -> R.string.error_payid_not_registere
|
||||
FailReason.PAY_ID_REQUEST_FAILED -> R.string.error_payid_request_failed
|
||||
FailReason.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.error_address_invalid_or_unsupported
|
||||
FailReason.ADDRESS_SAME_AS_WALLET -> R.string.error_address_same_as_wallet
|
||||
Error.IS_NOT_PAY_ID -> R.string.error_payid_verification_failed
|
||||
Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.error_payid_unsupported_by_blockchain
|
||||
Error.PAY_ID_NOT_REGISTERED -> R.string.error_payid_not_registere
|
||||
Error.PAY_ID_REQUEST_FAILED -> R.string.error_payid_request_failed
|
||||
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.error_address_invalid_or_unsupported
|
||||
Error.ADDRESS_SAME_AS_WALLET -> R.string.error_address_same_as_wallet
|
||||
else -> null
|
||||
}
|
||||
return if (resId == null) null
|
||||
|
|
@ -54,9 +63,10 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
|
|||
val til = fg.tilAddressOrPayId
|
||||
val parsedError = parseError(til.context, state.error)
|
||||
|
||||
til.parent?.parent?.beginDelayedTransition()
|
||||
til.error = parsedError
|
||||
til.isErrorEnabled = parsedError != null
|
||||
til.helperText = state.walletAddress
|
||||
til.helperText = state.recipientWalletAddress
|
||||
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
|
||||
|
||||
// prevent cycling
|
||||
|
|
@ -65,28 +75,23 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
|
|||
et.update(state.etFieldValue)
|
||||
}
|
||||
|
||||
private fun handleFeeLayoutState(fg: Fragment, layoutState: FeeLayoutState) {
|
||||
if (fg.llFeeContainer.visibility != layoutState.visibility) {
|
||||
val rotationAngle = if (fg.imvExpandCollapse.rotation == 0f) 180f else 0f
|
||||
fg.imvExpandCollapse.rotation = rotationAngle
|
||||
|
||||
(fg.llFeeContainer.parent?.parent as? ViewGroup)?.beginDelayedTransition()
|
||||
fg.llFeeContainer.visibility = layoutState.visibility
|
||||
private fun handleAmountState(fg: BaseStoreFragment, state: AmountState) {
|
||||
when (state.error) {
|
||||
AmountAction.Error.FEE_GREATER_THAN_AMOUNT -> {
|
||||
fg.amountContainer.parent?.beginDelayedTransition()
|
||||
fg.tilAmountToSend.enableError(true, fg.getString(R.string.error_fee_greater_than_amount))
|
||||
}
|
||||
AmountAction.Error.AMOUNT_WITH_FEE_GREATER_THAN_BALANCE -> {
|
||||
fg.amountContainer.parent?.beginDelayedTransition()
|
||||
fg.tilAmountToSend.enableError(true, fg.getString(R.string.error_amount_with_fee_greater_than_balance))
|
||||
}
|
||||
null -> {
|
||||
if (fg.tilAmountToSend.isErrorEnabled) fg.amountContainer.parent?.beginDelayedTransition()
|
||||
fg.tilAmountToSend.enableError(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (fg.swIncludeFee.isChecked != layoutState.includeFeeIsChecked) {
|
||||
fg.swIncludeFee.isChecked = layoutState.includeFeeIsChecked
|
||||
}
|
||||
|
||||
if (fg.chipGroup.checkedChipId != layoutState.selectedFeeId) {
|
||||
fg.chipGroup.check(layoutState.selectedFeeId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAmountState(fg: Fragment, state: AmountState) {
|
||||
fg.tilAmountToSend.enableError(state.amountIsOverBalance)
|
||||
|
||||
val amountToSend = state.etAmountFieldValue
|
||||
val amountToSend = state.viewAmountValue
|
||||
fg.tvAmountToSendShadow.text = amountToSend
|
||||
if (amountToSend.length > 10) {
|
||||
// post is needed to wait for text size changes
|
||||
|
|
@ -104,11 +109,134 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
|
|||
}
|
||||
|
||||
fg.tvAmountCurrency.update(state.mainCurrency.displayedValue)
|
||||
(fg as? SendFragment)?.let { it.saveMainCurrency(state.mainCurrency.value) }
|
||||
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.value)
|
||||
|
||||
val balanceText = fg.getString(R.string.send_balance,
|
||||
state.mainCurrency.displayedValue,
|
||||
state.balance.toPlainString())
|
||||
state.viewBalanceValue)
|
||||
fg.tvBalance.update(balanceText)
|
||||
}
|
||||
|
||||
private fun handleFeeState(fg: BaseStoreFragment, state: FeeState) {
|
||||
var delayedTransitionScheduled = false
|
||||
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.let {
|
||||
it.show(state.mainLayoutIsVisible) {
|
||||
(it.parent as? ViewGroup)?.beginDelayedTransition()
|
||||
delayedTransitionScheduled = true
|
||||
}
|
||||
}
|
||||
|
||||
fg.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
|
||||
fg.llFeeControlsContainer.show(state.controlsLayoutIsVisible) {
|
||||
if (!delayedTransitionScheduled) {
|
||||
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
|
||||
}
|
||||
}
|
||||
|
||||
fg.chipGroup.show(state.feeChipGroupIsVisible) {
|
||||
if (!delayedTransitionScheduled) {
|
||||
fg.llFeeControlsContainer.parent?.parent?.beginDelayedTransition()
|
||||
}
|
||||
}
|
||||
|
||||
if (fg.swIncludeFee.isChecked != state.feeIsIncluded) {
|
||||
fg.swIncludeFee.isChecked = state.feeIsIncluded
|
||||
}
|
||||
|
||||
if (state.error == FeeAction.Error.REQUEST_FAILED) {
|
||||
fg.showRetrySnackbar(fg.requireContext().getString(R.string.error_fee_request_failed)) {
|
||||
store.dispatch(FeeAction.RequestFee)
|
||||
}
|
||||
}
|
||||
|
||||
val chipId = FeeUiHelper.feeToId(state.selectedFeeType)
|
||||
if (fg.chipGroup.checkedChipId != chipId && chipId != 0) fg.chipGroup.check(chipId)
|
||||
}
|
||||
|
||||
private fun handleReceiptState(fg: BaseStoreFragment, state: ReceiptState) {
|
||||
val mainLayout = fg.clReceiptContainer as ViewGroup
|
||||
val totalLayout = fg.llTotal as ViewGroup
|
||||
val totalTokenLayout = fg.flTotalTokenCrypto as ViewGroup
|
||||
fun getString(id: Int): String = mainLayout.context.getString(id)
|
||||
|
||||
mainLayout.show(state.mainLayoutIsVisible)
|
||||
when (state.visibleTypeOfReceipt) {
|
||||
ReceiptLayoutType.FIAT -> {
|
||||
val receipt = state.fiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
fg.tvReceiptAmountValue.update("${receipt.amountFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
fg.tvReceiptFeeValue.update("${receipt.feeFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
totalLayout.tvTotalValue.update("${receipt.totalFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold { append(receipt.willSentCrypto.toPlainString()) }.append(" ")
|
||||
.append(receipt.symbols.crypto).append(" ")
|
||||
.append(getString(R.string.send_total_will_be_sent))
|
||||
totalLayout.tvWillBeSentValue.update(willSent)
|
||||
|
||||
}
|
||||
ReceiptLayoutType.CRYPTO -> {
|
||||
val receipt = state.crypto ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
fg.tvReceiptAmountValue.update("${receipt.amountCrypto.toPlainString()} ${receipt.symbols.crypto}")
|
||||
fg.tvReceiptFeeValue.update("${receipt.feeCrypto.toPlainString()} ${receipt.symbols.crypto}")
|
||||
totalLayout.tvTotalValue.update("${receipt.totalCrypto.toPlainString()} ${receipt.symbols.crypto}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold {
|
||||
append(getString(R.string.sign_rough))
|
||||
append(" ")
|
||||
append(receipt.willSentFiat.toPlainString())
|
||||
append(" ")
|
||||
append(receipt.symbols.fiat)
|
||||
}
|
||||
totalLayout.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
ReceiptLayoutType.TOKEN_FIAT -> {
|
||||
val receipt = state.tokenFiat ?: return
|
||||
|
||||
totalLayout.show(true)
|
||||
totalTokenLayout.show(false)
|
||||
fg.tvReceiptAmountValue.update("${receipt.amountFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
fg.tvReceiptFeeValue.update("${receipt.feeFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
totalLayout.tvTotalValue.update("${receipt.totalFiat.toPlainString()} ${receipt.symbols.fiat}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold {
|
||||
append(receipt.symbols.token)
|
||||
append(" ")
|
||||
append(receipt.willSentFeeCrypto.toPlainString())
|
||||
}.append(" ").append(getString(R.string.generic_and)).append(" ")
|
||||
.bold {
|
||||
append(receipt.symbols.crypto).append(" ")
|
||||
append(receipt.willSentFeeCrypto.toPlainString()).append(" ")
|
||||
}
|
||||
.append(mainLayout.context.getString(R.string.send_total_will_be_sent))
|
||||
totalLayout.tvWillBeSentValue.update(willSent)
|
||||
}
|
||||
ReceiptLayoutType.TOKEN_CRYPTO -> {
|
||||
val receipt = state.tokenCrypto ?: return
|
||||
|
||||
totalLayout.show(false)
|
||||
totalTokenLayout.show(true)
|
||||
|
||||
fg.tvReceiptAmountValue.update("${receipt.amountToken.toPlainString()} ${receipt.symbols.token}")
|
||||
fg.tvReceiptFeeValue.update("${receipt.feeCrypto.toPlainString()} ${receipt.symbols.crypto}")
|
||||
|
||||
val willSent = SpannableStringBuilder()
|
||||
.bold {
|
||||
append(getString(R.string.sign_rough))
|
||||
append(" ")
|
||||
append(receipt.totalFiat.toPlainString())
|
||||
append(" ")
|
||||
append(receipt.symbols.fiat)
|
||||
}
|
||||
totalTokenLayout.tvTotalTokenCryptoValue.update(willSent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@
|
|||
android:layout_gravity="center"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:padding="5dp"
|
||||
android:rotation="180"
|
||||
app:srcCompat="@drawable/ic_angle_bracket_up" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="24dp"
|
||||
|
|
@ -39,23 +40,28 @@
|
|||
|
||||
<include
|
||||
android:id="@+id/clNetworkFee"
|
||||
layout="@layout/layout_send_network_fee"
|
||||
layout="@layout/layout_send_fee"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp" />
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
android:layout_weight="1"
|
||||
android:minHeight="1dp" />
|
||||
|
||||
<include
|
||||
android:id="@+id/clNetworkFee"
|
||||
layout="@layout/layout_send_total"
|
||||
android:id="@+id/clReceiptContainer"
|
||||
layout="@layout/layout_send_receipt"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="24dp" />
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llBottomButtonContainer"
|
||||
|
|
|
|||
137
app/src/main/res/layout/layout_receipt_total.xml
Normal file
137
app/src/main/res/layout/layout_receipt_total.xml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/llTotalContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llTotal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:text="@string/send_total"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextSwitcher
|
||||
android:id="@+id/tvTotalValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:inAnimation="@anim/slide_in_right"
|
||||
android:outAnimation="@android:anim/slide_out_right">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="usd" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="usd" />
|
||||
</TextSwitcher>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextSwitcher
|
||||
android:id="@+id/tvWillBeSentValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:inAnimation="@anim/slide_in_right"
|
||||
android:outAnimation="@android:anim/slide_out_right">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/send_total_will_be_sent"
|
||||
android:textColor="@color/darkGray1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/send_total_will_be_sent"
|
||||
android:textColor="@color/darkGray1" />
|
||||
</TextSwitcher>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/flTotalTokenCrypto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotalTokenCrypto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:text="@string/send_total"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextSwitcher
|
||||
android:id="@+id/tvTotalTokenCryptoValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:inAnimation="@anim/slide_in_right"
|
||||
android:outAnimation="@android:anim/slide_out_right">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="usd" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="usd" />
|
||||
|
||||
</TextSwitcher>
|
||||
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -41,7 +41,7 @@
|
|||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/tilAmountToSend"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="82dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
|
|
@ -66,12 +66,12 @@
|
|||
android:id="@+id/tvAmountCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="26dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:inAnimation="@anim/slide_in_up"
|
||||
android:outAnimation="@anim/slide_out_down"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/flAmountToSend"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -82,7 +82,8 @@
|
|||
android:textAllCaps="true"
|
||||
android:textColor="@color/blue"
|
||||
android:textSize="32sp"
|
||||
app:drawableEndCompat="@drawable/ic_arrows_up_down" />
|
||||
app:drawableEndCompat="@drawable/ic_arrows_up_down"
|
||||
tools:text="USD" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
@ -112,13 +113,15 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textColor="@color/darkGray1" />
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textColor="@color/darkGray1" />
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</TextSwitcher>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:text="@string/send_fee"
|
||||
android:textSize="13sp"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/flExpandCollapse"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
|
@ -27,11 +27,10 @@
|
|||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llFeeContainer"
|
||||
android:id="@+id/llFeeControlsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/flExpandCollapse">
|
||||
|
|
@ -43,6 +42,7 @@
|
|||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:translationY="-8dp"
|
||||
app:selectionRequired="true"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="true">
|
||||
|
||||
|
|
@ -75,9 +75,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/send_fee_include" />
|
||||
android:text="@string/send_fee_include"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
106
app/src/main/res/layout/layout_send_receipt.xml
Normal file
106
app/src/main/res/layout/layout_send_receipt.xml
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/clReceiptContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvReceiptAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_total_amount"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextSwitcher
|
||||
android:id="@+id/tvReceiptAmountValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inAnimation="@anim/slide_in_right"
|
||||
android:outAnimation="@android:anim/slide_out_right"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textStyle="bold"
|
||||
tools:text="75.00 usd" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textStyle="bold"
|
||||
tools:text="75.00 usd" />
|
||||
|
||||
</TextSwitcher>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvReceiptFee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/send_total_fee"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvReceiptAmount" />
|
||||
|
||||
<TextSwitcher
|
||||
android:id="@+id/tvReceiptFeeValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inAnimation="@anim/slide_in_right"
|
||||
android:outAnimation="@android:anim/slide_out_right"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvReceiptFee">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
tools:text="0.03 usd" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
tools:text="0.03 usd" />
|
||||
|
||||
</TextSwitcher>
|
||||
|
||||
<View
|
||||
android:id="@+id/delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@color/separatorGrey2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvReceiptFee"
|
||||
tools:layout_editor_absoluteX="0dp" />
|
||||
|
||||
<include
|
||||
android:id="@+id/llTotalContainer"
|
||||
layout="@layout/layout_receipt_total"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/delimiter" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAmount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_total_amount"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView9"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvAmountCurrency"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="75.00" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvAmountCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="true"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="usd" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFee"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/send_total_fee"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvAmount" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFeeValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvFeeCurrency"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvFeeCurrency"
|
||||
tools:text="0.003" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvFeeCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/darkGray1"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvFee"
|
||||
tools:text="usd" />
|
||||
|
||||
<View
|
||||
android:id="@+id/delimiter"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@color/separatorGrey2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvFee"
|
||||
tools:layout_editor_absoluteX="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/send_total"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/delimiter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotalValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvTotalCurrency"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvTotalCurrency"
|
||||
tools:text="75.40" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTotalCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvTotal"
|
||||
tools:text="usd" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSentValue"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textColor="@color/lightGray4"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvSentCurrency"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvWillBeSent"
|
||||
tools:text="0.0045544645" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSentCurrency"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:textColor="@color/lightGray4"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/tvWillBeSent"
|
||||
app:layout_constraintTop_toTopOf="@+id/tvWillBeSent"
|
||||
tools:text="BTC" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvWillBeSent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/send_total_will_be_sent"
|
||||
android:textColor="@color/lightGray4"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tvTotal" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
<string name="app_name" translatable="false">Tangem Tap</string>
|
||||
|
||||
<string name="generic_done">Done</string>
|
||||
<string name="generic_retry">Retry</string>
|
||||
<string name="generic_and">and</string>
|
||||
|
||||
<string name="notification_address_copied">Address was successfully copied</string>
|
||||
<string name="notification_no_internet">No internet connection</string>
|
||||
<string name="notification_no_internet_retry">Retry</string>
|
||||
<string name="generic_done">Done</string>
|
||||
<string name="notification_no_internet_retry">@string/generic_retry</string>
|
||||
|
||||
<string name="home_do_you_have_card">Welcome to Tangem.\nDo you have one of our cards?</string>
|
||||
<string name="home_button_yes">Yes!</string>
|
||||
|
|
@ -47,6 +50,11 @@
|
|||
<string name="error_payid_request_failed">PayID request failed</string>
|
||||
<string name="error_address_invalid_or_unsupported">Address is invalid or unsupported by blockchain</string>
|
||||
<string name="error_address_same_as_wallet">Address is the same as wallet address</string>
|
||||
<string name="error_fee_request_failed">Network fee request is failed</string>
|
||||
<string name="error_fee_greater_than_amount">Fee greater than amount</string>
|
||||
<string name="error_amount_with_fee_greater_than_balance">Amount with fee greater than balance</string>
|
||||
<string name="error_insufficient_balance">Insufficient balance</string>
|
||||
<string name="error_blockchain_internal">Blockchain internal error</string>
|
||||
|
||||
|
||||
<string name="send_title">Send</string>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<resources>
|
||||
|
||||
<string name="sign_rough">≈</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue