diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 71f44fadf5..d4d8847066 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -52,4 +52,19 @@ fun BigDecimal.toFiatString(rateValue: BigDecimal): String? { return "≈ USD  $fiatValue" } -fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() \ No newline at end of file +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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt index a9af6ebb0c..5774e94b61 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index c8d2a2c41c..11d5aa6ca4 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt index a4cc3b9e65..54d98f110d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 0ce1e1e244..b98107376d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 045c802a04..9e3bdf3e9b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 9fdfca3cd7..0b76964de9 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -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 { + return withContext(Dispatchers.IO) { + suspendCoroutine { continuation -> + tangemSdk.startSessionWithRunnable(SendTask(walletManager, recipientAddress, amountToSend, feeAmount)) { + continuation.resume(it) + } + } + } + } + private suspend fun runTaskAsync( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null ): CompletionResult = diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 1141048a49..29147d93c6 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt new file mode 100644 index 0000000000..e5cdbf959a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/SendTask.kt @@ -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 { + + override val requiresPin2: Boolean = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> 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, cardId: String): CompletionResult = + 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] + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt index 20669d7f3b..83182d57ac 100644 --- a/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/BaseStoreFragment.kt @@ -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>() 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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt deleted file mode 100644 index aae1e5a5b7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendMiddleware.kt +++ /dev/null @@ -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 = { 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], "") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt deleted file mode 100644 index 5eb886c5dd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendReducer.kt +++ /dev/null @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index b5d5d87563..1f76fa2399 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -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) : 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt deleted file mode 100644 index 542699ca22..0000000000 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendState.kt +++ /dev/null @@ -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( - 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 = 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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt new file mode 100644 index 0000000000..60e0077661 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressPayIdMiddleware.kt @@ -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], "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt new file mode 100644 index 0000000000..eb86509ec1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt new file mode 100644 index 0000000000..331a83fe37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -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 { + return feeStandard(blockchain) +// return feeSingle(blockchain) +// return feeStellar(blockchain) +// return feeZero(blockchain) + } + + suspend fun feeStellar(blockchain: Blockchain): List = listOf(Amount(0.0001.toBigDecimal(), blockchain)) + + suspend fun feeZero(blockchain: Blockchain): List = listOf(Amount(BigDecimal.ZERO, blockchain)) + + suspend fun feeStandard(blockchain: Blockchain): List = listOf( + Amount(0.001500.toBigDecimal(), blockchain), + Amount(0.0030.toBigDecimal(), blockchain), + Amount(0.0045001.toBigDecimal(), blockchain) + ) + + suspend fun feeStandardBig(blockchain: Blockchain): List = listOf( + Amount(1.76.toBigDecimal(), blockchain), + Amount(2.30.toBigDecimal(), blockchain), + Amount(3.45.toBigDecimal(), blockchain) + ) + + suspend fun feeSingle(blockchain: Blockchain): List = listOf(Amount(0.0015.toBigDecimal(), blockchain)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt new file mode 100644 index 0000000000..03645a9444 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -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 = { 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)) + } + } + } + } + +} + diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt new file mode 100644 index 0000000000..72b7e5e05b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AddressPayIdReducer.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt new file mode 100644 index 0000000000..c4019ff028 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt new file mode 100644 index 0000000000..883ca9f708 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -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?): Value? { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt new file mode 100644 index 0000000000..0268a051d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendReducer.kt new file mode 100644 index 0000000000..4f5d12a94b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendReducer.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt new file mode 100644 index 0000000000..1b6583fdd5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/AddressPayIdState.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt new file mode 100644 index 0000000000..2a0f019897 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -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? = null, + val currentFee: Value? = 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt new file mode 100644 index 0000000000..7efe70ff14 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/ReceiptState.kt @@ -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? = 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt new file mode 100644 index 0000000000..8a23874e94 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -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 = 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( + val value: T, + val displayedValue: String +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index c5cb65e888..1fd7e7664d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -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 = callbackFlow { +fun EditText.inputtedTextAsFlow(): Flow = 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 + } + } + } +} + diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt index 58949c7df7..aa129ebbb5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/FragmentStateSubscriber.kt @@ -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(fragment: Fragment) : StoreSubscriber { - private val weakFragment: WeakReference = WeakReference(fragment) +abstract class FragmentStateSubscriber(fragment: BaseStoreFragment) : StoreSubscriber { + private val weakFragment: WeakReference = 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 diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 9111bf2703..ec819fae5d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -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(fragment) { +class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber(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 { + 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(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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/btn_expand_collapse.xml b/app/src/main/res/layout/btn_expand_collapse.xml index 8b1bbc2529..76f0338c98 100644 --- a/app/src/main/res/layout/btn_expand_collapse.xml +++ b/app/src/main/res/layout/btn_expand_collapse.xml @@ -18,7 +18,6 @@ android:layout_gravity="center" android:background="?selectableItemBackgroundBorderless" android:padding="5dp" - android:rotation="180" app:srcCompat="@drawable/ic_angle_bracket_up" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 6002054678..3817a01615 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -1,6 +1,7 @@ + android:layout_marginTop="16dp" + android:visibility="gone" + tools:visibility="visible" /> + android:layout_weight="1" + android:minHeight="1dp" /> + android:layout_marginTop="32dp" + android:layout_marginBottom="24dp" + android:visibility="gone" + tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_send_amount.xml b/app/src/main/res/layout/layout_send_amount.xml index 09b79ee038..4a6c5066e6 100644 --- a/app/src/main/res/layout/layout_send_amount.xml +++ b/app/src/main/res/layout/layout_send_amount.xml @@ -41,7 +41,7 @@ + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent"> + app:drawableEndCompat="@drawable/ic_arrows_up_down" + tools:text="USD" /> + android:textColor="@color/darkGray1" + android:textSize="16sp" /> + android:textColor="@color/darkGray1" + android:textSize="16sp" /> diff --git a/app/src/main/res/layout/layout_send_network_fee.xml b/app/src/main/res/layout/layout_send_fee.xml similarity index 93% rename from app/src/main/res/layout/layout_send_network_fee.xml rename to app/src/main/res/layout/layout_send_fee.xml index e9cdabcf7f..ebe9b5c629 100644 --- a/app/src/main/res/layout/layout_send_network_fee.xml +++ b/app/src/main/res/layout/layout_send_fee.xml @@ -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" /> @@ -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" /> - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml new file mode 100644 index 0000000000..4b68177593 --- /dev/null +++ b/app/src/main/res/layout/layout_send_receipt.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_send_total.xml b/app/src/main/res/layout/layout_send_total.xml deleted file mode 100644 index 83ca437a9e..0000000000 --- a/app/src/main/res/layout/layout_send_total.xml +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8fa6038ac..a871a0485b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,10 +2,13 @@ Tangem Tap + Done + Retry + and + Address was successfully copied No internet connection - Retry - Done + @string/generic_retry Welcome to Tangem.\nDo you have one of our cards? Yes! @@ -47,6 +50,11 @@ PayID request failed Address is invalid or unsupported by blockchain Address is the same as wallet address + Network fee request is failed + Fee greater than amount + Amount with fee greater than balance + Insufficient balance + Blockchain internal error Send diff --git a/app/src/main/res/values/strings_untranslatable.xml b/app/src/main/res/values/strings_untranslatable.xml index 25fe7ea98a..ff1f090a6f 100644 --- a/app/src/main/res/values/strings_untranslatable.xml +++ b/app/src/main/res/values/strings_untranslatable.xml @@ -1,4 +1,5 @@ + ≈