Updated on 2026-08-14
This commit is contained in:
parent
957bc23951
commit
a0ec01abfe
17 changed files with 400 additions and 177 deletions
|
|
@ -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.reducers.SendReducer
|
||||
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
|
||||
import com.tangem.tap.features.wallet.redux.WalletReducer
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ fun appReducer(action: Action, state: AppState?): AppState {
|
|||
navigationState = NavigationReducer.reduce(action, state),
|
||||
globalState = globalReducer(action, state),
|
||||
walletState = WalletReducer.reduce(action, state),
|
||||
sendState = SendReducer.reduce(action, state.sendState)
|
||||
sendState = SendScreenReducer.reduce(action, state.sendState)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.tap.domain.TapError
|
||||
|
|
@ -32,6 +33,12 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
|
|||
showNotification(it.context.getString(message))
|
||||
}
|
||||
}
|
||||
|
||||
fun showToastNotification(message: Int) {
|
||||
baseLayout.get()?.let {
|
||||
Toast.makeText(it.context, it.context.getString(message), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
|
|
@ -40,6 +47,9 @@ val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
|
|||
if (action is NotificationAction) {
|
||||
notificationsHandler?.showNotification(action.messageResource)
|
||||
}
|
||||
if (action is ToastNotificationAction) {
|
||||
notificationsHandler?.showToastNotification(action.messageResource)
|
||||
}
|
||||
if (action is ErrorAction) {
|
||||
notificationsHandler?.showNotification(action.error.localizedMessage)
|
||||
}
|
||||
|
|
@ -48,6 +58,10 @@ val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
|
|||
}
|
||||
}
|
||||
|
||||
interface ToastNotificationAction : Action {
|
||||
val messageResource: Int
|
||||
}
|
||||
|
||||
interface NotificationAction : Action {
|
||||
val messageResource: Int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.merchant.common.toggleWidget
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface ToggleState
|
||||
|
||||
interface StateModifier {
|
||||
fun stateChanged(container: ViewGroup, view: View, state: ToggleState)
|
||||
}
|
||||
|
||||
interface ToggleView {
|
||||
val mainViewModifiers: MutableList<StateModifier>
|
||||
val toggleViewModifiers: MutableList<StateModifier>
|
||||
|
||||
fun setState(state: ToggleState, andApply: Boolean = true)
|
||||
fun applyState()
|
||||
fun getView(): View
|
||||
fun getMainView(): View
|
||||
fun getToggleView(): View
|
||||
}
|
||||
|
||||
class ToggleWidget : ToggleView {
|
||||
private val container: ViewGroup
|
||||
private val mainView: View
|
||||
private val toggleView: View
|
||||
|
||||
private var state: ToggleState
|
||||
|
||||
constructor(
|
||||
container: ViewGroup,
|
||||
mainView: View,
|
||||
toggleView: View,
|
||||
initialState: ToggleState,
|
||||
mainViewModifier: List<StateModifier> = mutableListOf(),
|
||||
loadingViewModifier: List<StateModifier> = mutableListOf()
|
||||
) {
|
||||
this.container = container
|
||||
this.mainView = mainView
|
||||
this.toggleView = toggleView
|
||||
this.state = initialState
|
||||
this.mainViewModifiers.addAll(mainViewModifier)
|
||||
this.toggleViewModifiers.addAll(loadingViewModifier)
|
||||
}
|
||||
|
||||
constructor(
|
||||
container: ViewGroup,
|
||||
mainViewId: Int,
|
||||
toggleViewId: Int,
|
||||
initialState: ToggleState,
|
||||
mainViewModifier: List<StateModifier> = mutableListOf(),
|
||||
loadingViewModifier: List<StateModifier> = mutableListOf()
|
||||
) {
|
||||
this.container = container
|
||||
this.mainView = container.findViewById(mainViewId)
|
||||
this.toggleView = container.findViewById(toggleViewId)
|
||||
this.state = initialState
|
||||
this.mainViewModifiers.addAll(mainViewModifier)
|
||||
this.toggleViewModifiers.addAll(loadingViewModifier)
|
||||
}
|
||||
|
||||
override val mainViewModifiers: MutableList<StateModifier> = mutableListOf()
|
||||
|
||||
override val toggleViewModifiers: MutableList<StateModifier> = mutableListOf()
|
||||
|
||||
override fun setState(state: ToggleState, andApply: Boolean) {
|
||||
this.state = state
|
||||
if (andApply) applyState()
|
||||
}
|
||||
|
||||
override fun applyState() {
|
||||
mainViewModifiers.forEach { it.stateChanged(container, mainView, state) }
|
||||
toggleViewModifiers.forEach { it.stateChanged(container, toggleView, state) }
|
||||
}
|
||||
|
||||
override fun getView(): View = container
|
||||
|
||||
override fun getMainView(): View = mainView
|
||||
|
||||
override fun getToggleView(): View = toggleView
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.tangem.tap.common.toggleWidget
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.tangem.merchant.common.toggleWidget.StateModifier
|
||||
import com.tangem.merchant.common.toggleWidget.ToggleState
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
sealed class ProgressState : ToggleState {
|
||||
class Progress : ProgressState()
|
||||
class None : ProgressState()
|
||||
}
|
||||
|
||||
class ReplaceTextStateModifier(
|
||||
private val initialText: String,
|
||||
private val replaceText: String = ""
|
||||
) : StateModifier {
|
||||
|
||||
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
|
||||
val tv = view as? TextView ?: return
|
||||
|
||||
when (state) {
|
||||
is ProgressState.Progress -> {
|
||||
tv.text = replaceText
|
||||
}
|
||||
is ProgressState.None -> {
|
||||
tv.text = initialText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TextViewDrawableStateModifier(
|
||||
private val initialDrawable: Drawable?,
|
||||
private val replaceDrawable: Drawable?,
|
||||
private val position: Int
|
||||
) : StateModifier {
|
||||
companion object {
|
||||
val LEFT = 1
|
||||
val RIGHT = 2
|
||||
}
|
||||
|
||||
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
|
||||
val drawable = when (state) {
|
||||
is ProgressState.Progress -> replaceDrawable
|
||||
is ProgressState.None -> initialDrawable
|
||||
else -> null
|
||||
}
|
||||
val drawableChanger = getChanger(view)
|
||||
drawableChanger?.change(drawable, position)
|
||||
}
|
||||
|
||||
private fun getChanger(view: View): DrawableChanger? = when (view) {
|
||||
is MaterialButton -> MaterialButtonChanger(view)
|
||||
is Button -> TextViewChanger(view)
|
||||
is TextView -> TextViewChanger(view)
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal interface DrawableChanger {
|
||||
fun change(drawable: Drawable?, position: Int)
|
||||
}
|
||||
|
||||
internal class TextViewChanger(private val view: TextView) : DrawableChanger {
|
||||
override fun change(drawable: Drawable?, position: Int) {
|
||||
when (position) {
|
||||
LEFT -> setLeft(drawable)
|
||||
RIGHT -> setRight(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLeft(drawable: Drawable?) {
|
||||
if (drawable == null) {
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, 0, 0, 0, 0)
|
||||
} else {
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, drawable, null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setRight(drawable: Drawable?) {
|
||||
if (drawable == null) {
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, 0, 0, 0, 0)
|
||||
} else {
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(view, null, null, drawable, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class MaterialButtonChanger(private val view: MaterialButton) : DrawableChanger {
|
||||
override fun change(drawable: Drawable?, position: Int) {
|
||||
when (position) {
|
||||
LEFT -> setLeft(drawable)
|
||||
RIGHT -> setRight(drawable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLeft(drawable: Drawable?) {
|
||||
view.iconGravity = MaterialButton.ICON_GRAVITY_START
|
||||
view.icon = drawable
|
||||
}
|
||||
|
||||
private fun setRight(drawable: Drawable?) {
|
||||
view.iconGravity = MaterialButton.ICON_GRAVITY_END
|
||||
view.icon = drawable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShowHideStateModifier(
|
||||
private val isShowOnLoading: Boolean = true,
|
||||
private val typeOfHiding: Int = View.INVISIBLE
|
||||
) : StateModifier {
|
||||
|
||||
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
|
||||
container.beginDelayedTransition()
|
||||
view.visibility = when (state) {
|
||||
is ProgressState.Progress -> if (isShowOnLoading) View.VISIBLE else typeOfHiding
|
||||
is ProgressState.None -> if (isShowOnLoading) typeOfHiding else View.VISIBLE
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClickableStateModifier(
|
||||
private val isClickableOnLoading: Boolean = false
|
||||
) : StateModifier {
|
||||
|
||||
override fun stateChanged(container: ViewGroup, view: View, state: ToggleState) {
|
||||
view.isClickable = when (state) {
|
||||
is ProgressState.Progress -> isClickableOnLoading
|
||||
is ProgressState.None -> !isClickableOnLoading
|
||||
else -> return
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ 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
|
||||
|
|
@ -11,7 +9,6 @@ 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.*
|
||||
|
|
@ -31,21 +28,6 @@ class TangemSdkManager(val activity: ComponentActivity) {
|
|||
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask())
|
||||
}
|
||||
|
||||
suspend fun send(
|
||||
walletManager: WalletManager,
|
||||
recipientAddress: String,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount
|
||||
): CompletionResult<CommandResponse> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
suspendCoroutine { continuation ->
|
||||
tangemSdk.startSessionWithRunnable(SendTask(walletManager, recipientAddress, amountToSend, feeAmount)) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : CommandResponse> runTaskAsync(
|
||||
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
|
||||
): CompletionResult<T> =
|
||||
|
|
|
|||
|
|
@ -10,5 +10,6 @@ sealed class TapError(@StringRes val localizedMessage: Int): Throwable() {
|
|||
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)
|
||||
object BlockchainInternalError: TapError(R.string.error_blockchain_internal)
|
||||
object UnknownError: TapError(R.string.error_unknown)
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
package com.tangem.tap.domain.tasks
|
||||
|
||||
import com.tangem.CardSession
|
||||
import com.tangem.CardSessionRunnable
|
||||
import com.tangem.TangemError
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.commands.SignCommand
|
||||
import com.tangem.commands.SignResponse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class SendTask(
|
||||
private val walletManager: WalletManager,
|
||||
private val recipientAddress: String,
|
||||
private val amountToSend: Amount,
|
||||
private val feeAmount: Amount,
|
||||
) : CardSessionRunnable<CommandResponse> {
|
||||
|
||||
override val requiresPin2: Boolean = false
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<CommandResponse>) -> Unit) {
|
||||
val txSender = walletManager as TransactionSender
|
||||
|
||||
val verifyResult = walletManager.validateTransaction(amountToSend, feeAmount)
|
||||
if (verifyResult.isNotEmpty()) {
|
||||
callback(CompletionResult.Failure(InsufficientBalance()))
|
||||
return
|
||||
}
|
||||
val txData = walletManager.createTransaction(amountToSend, feeAmount, recipientAddress)
|
||||
|
||||
scope.launch {
|
||||
when (val result = txSender.send(txData, SessionTransactionSigner(session))) {
|
||||
is SimpleResult.Success -> callback(CompletionResult.Success(SendResponse()))
|
||||
is SimpleResult.Failure -> {
|
||||
callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(result.error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SendResponse : CommandResponse
|
||||
|
||||
class SessionTransactionSigner(
|
||||
private val session: CardSession
|
||||
) : TransactionSigner {
|
||||
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): CompletionResult<SignResponse> =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
Timber.d("sign transaction...")
|
||||
SignCommand(hashes).run(session) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SendError : TangemError {
|
||||
}
|
||||
|
||||
class UnknownError : SendError() {
|
||||
override val code: Int = 1000
|
||||
override var customMessage: String = "Unknown error"
|
||||
}
|
||||
|
||||
open class ThrowableError(throwable: Throwable?) : SendError() {
|
||||
override val code: Int = 1001
|
||||
override var customMessage: String = throwable?.localizedMessage ?: "Unknown exception"
|
||||
}
|
||||
|
||||
class InsufficientBalance(
|
||||
override var customMessage: String = "Insufficient balance"
|
||||
) : SendError() {
|
||||
override val code: Int = 1021
|
||||
}
|
||||
|
||||
class BlockchainInternalError(
|
||||
override var customMessage: String
|
||||
) : SendError() {
|
||||
override val code: Int = 2000
|
||||
}
|
||||
|
||||
class BlockchainInternalErrorConverter {
|
||||
companion object {
|
||||
|
||||
private val stellarInternalErrors = mapOf(
|
||||
"tx_bad_seq" to "Sequence number does not match source account",
|
||||
"tx_too_late" to "The ledger closeTime was after the maxTime",
|
||||
"tx_failedop_no_destination" to "The destination account does not exist",
|
||||
"tx_no_source_account" to "Source account not found"
|
||||
)
|
||||
|
||||
fun convert(throwable: Throwable?): TangemError {
|
||||
val message = throwable?.message ?: return ThrowableError(throwable)
|
||||
|
||||
val customMessage = getInternalBlockchainErrorMessage(message)
|
||||
return if (customMessage == null) ThrowableError(throwable)
|
||||
else BlockchainInternalError(customMessage)
|
||||
}
|
||||
|
||||
private fun getInternalBlockchainErrorMessage(message: String): String? {
|
||||
return stellarInternalErrors[message]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,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.common.redux.ToastNotificationAction
|
||||
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 com.tangem.tap.features.send.redux.states.SendButtonState
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -38,7 +41,7 @@ sealed class AddressPayIdVerifyAction : SendScreenAction {
|
|||
}
|
||||
|
||||
data class VerifyClipboard(val data: String?) : AddressPayIdVerifyAction()
|
||||
data class ChangePasteBtnEnableState(val isEnabled: Boolean): AddressPayIdVerifyAction()
|
||||
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressPayIdVerifyAction()
|
||||
|
||||
sealed class PayIdVerification : AddressPayIdVerifyAction() {
|
||||
data class SetError(val payId: String, val error: Error) : PayIdVerification()
|
||||
|
|
@ -106,16 +109,11 @@ sealed class SendActionUi : SendScreenActionUi {
|
|||
}
|
||||
|
||||
sealed class SendAction : SendScreenAction {
|
||||
enum class Error {
|
||||
INSUFFICIENT_BALANCE, BLOCKCHAIN_INTERNAL
|
||||
|
||||
data class ChangeSendButtonState(val state: SendButtonState) : SendAction()
|
||||
object SendSuccess : SendAction(), ToastNotificationAction {
|
||||
override val messageResource: Int = R.string.send_transaction_complete
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
data class SendError(override val error: TapError) : SendAction(), ErrorAction
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ 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.SendAction
|
||||
import com.tangem.tap.features.send.redux.states.MainCurrencyType
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
|
|
@ -43,6 +44,7 @@ class AmountMiddleware {
|
|||
dispatch(AmountAction.AmountVerification.SetError(checkResult.amount, checkResult.error))
|
||||
}
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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.features.send.redux.SendAction
|
||||
import com.tangem.tap.scope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -29,6 +30,7 @@ class RequestFeeMiddleware {
|
|||
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
|
||||
dispatch(FeeAction.ChangeLayoutVisibility(main = false, controls = true, chipGroup = true))
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +69,7 @@ class RequestFeeMiddleware {
|
|||
}
|
||||
}
|
||||
dispatch(ReceiptAction.RefreshReceipt)
|
||||
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,24 @@
|
|||
package com.tangem.tap.features.send.redux.middlewares
|
||||
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.extensions.Signer
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
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.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.features.send.redux.states.SendButtonState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.tap.tangemSdk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
|
|
@ -24,8 +31,8 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
|
|||
when (action) {
|
||||
is ChangeAddressOrPayId -> AddressPayIdMiddleware().handle(action.data, appState(), dispatch)
|
||||
is VerifyClipboard -> {
|
||||
AddressPayIdMiddleware().handle(action.data, appState()){
|
||||
when(it) {
|
||||
AddressPayIdMiddleware().handle(action.data, appState()) {
|
||||
when (it) {
|
||||
is AddressVerification.SetWalletAddress, is PayIdVerification.SetPayIdWalletAddress -> {
|
||||
dispatch(ChangePasteBtnEnableState(true))
|
||||
}
|
||||
|
|
@ -54,15 +61,44 @@ private fun verifyAndSendTransaction(appState: AppState?, dispatch: (Action) ->
|
|||
val feeAmount = Amount(sendState.feeState.getCurrentFee(), blockchain)
|
||||
val amountToSend = Amount(sendState.amountState.amountToSendCrypto, blockchain, recipientAddress)
|
||||
|
||||
val txSender = walletManager as TransactionSender
|
||||
|
||||
val verifyResult = walletManager.validateTransaction(amountToSend, feeAmount)
|
||||
if (verifyResult.isNotEmpty()) {
|
||||
dispatch(SendAction.SendError(TapError.InsufficientBalance))
|
||||
return
|
||||
}
|
||||
|
||||
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
|
||||
val txData = walletManager.createTransaction(amountToSend, feeAmount, 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))
|
||||
val result = txSender.send(txData, Signer(tangemSdk))
|
||||
withContext(Dispatchers.Main) {
|
||||
when (result) {
|
||||
is SimpleResult.Success -> {
|
||||
dispatch(SendAction.SendSuccess)
|
||||
dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
is SimpleResult.Failure -> {
|
||||
when (result.error) {
|
||||
is Throwable -> {
|
||||
val message = (result.error as Throwable).message
|
||||
when {
|
||||
message == null -> {
|
||||
dispatch(SendAction.SendError(TapError.UnknownError))
|
||||
}
|
||||
message.contains("50002") -> {
|
||||
// user was cancelled the operation by closing the Sdk bottom sheet
|
||||
}
|
||||
else -> {
|
||||
dispatch(SendAction.SendError(TapError.BlockchainInternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch(SendAction.ChangeSendButtonState(SendButtonState.ENABLED))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.tangem.tap.features.send.redux.states.IdStateHolder
|
|||
import com.tangem.tap.features.send.redux.states.SendState
|
||||
import com.tangem.tap.store
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
|
|
@ -17,7 +16,7 @@ interface SendInternalReducer {
|
|||
fun handle(action: SendScreenAction, sendState: SendState): SendState
|
||||
}
|
||||
|
||||
class SendReducer {
|
||||
class SendScreenReducer {
|
||||
companion object {
|
||||
fun reduce(incomingAction: Action, sendState: SendState): SendState {
|
||||
if (incomingAction is ReleaseSendState) return SendState()
|
||||
|
|
@ -29,18 +28,26 @@ class SendReducer {
|
|||
is AmountActionUi, is AmountAction -> AmountReducer()
|
||||
is FeeActionUi, is FeeAction -> FeeReducer()
|
||||
is ReceiptAction -> ReceiptReducer()
|
||||
is SendAction -> SendReducer()
|
||||
else -> EmptyReducer()
|
||||
}
|
||||
|
||||
|
||||
val newState = reducer.handle(action, sendState).copy(sendButtonIsEnabled = sendState.isReadyToSend())
|
||||
Timber.i("${newState.lastChangedStates}.")
|
||||
|
||||
return newState
|
||||
return reducer.handle(action, sendState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SendReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
|
||||
val result = when (action) {
|
||||
is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state)
|
||||
else -> return sendState
|
||||
}
|
||||
|
||||
return updateLastState(result, result)
|
||||
}
|
||||
}
|
||||
|
||||
private class EmptyReducer : SendInternalReducer {
|
||||
override fun handle(action: SendScreenAction, sendState: SendState): SendState = sendState
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ data class SendState(
|
|||
val amountState: AmountState = AmountState(),
|
||||
val feeState: FeeState = FeeState(),
|
||||
val receiptState: ReceiptState = ReceiptState(),
|
||||
val sendButtonIsEnabled: Boolean = false,
|
||||
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
|
||||
override val stateId: StateId = StateId.SEND_SCREEN
|
||||
) : SendScreenState {
|
||||
|
||||
|
|
@ -49,6 +49,12 @@ data class SendState(
|
|||
MainCurrencyType.FIAT -> 2
|
||||
MainCurrencyType.CRYPTO -> amount?.decimals ?: 0
|
||||
}
|
||||
|
||||
fun getButtonState(): SendButtonState = if (isReadyToSend()) SendButtonState.ENABLED else SendButtonState.DISABLED
|
||||
}
|
||||
|
||||
enum class SendButtonState {
|
||||
ENABLED, DISABLED, PROGRESS
|
||||
}
|
||||
|
||||
data class AmountState(
|
||||
|
|
|
|||
|
|
@ -8,14 +8,17 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.widget.EditText
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import com.tangem.merchant.common.toggleWidget.ToggleWidget
|
||||
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.getDrawableCompat
|
||||
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.common.toggleWidget.*
|
||||
import com.tangem.tap.features.send.BaseStoreFragment
|
||||
import com.tangem.tap.features.send.redux.*
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
|
||||
|
|
@ -40,12 +43,21 @@ import kotlinx.coroutines.flow.*
|
|||
*/
|
||||
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||
|
||||
lateinit var sendBtn: ToggleWidget
|
||||
|
||||
private fun initSendButtonStates() {
|
||||
sendBtn = ToggleWidget(flSendButtonContainer, btnSend, progress, ProgressState.None())
|
||||
sendBtn.setupSendButtonStateModifiers(requireContext())
|
||||
sendBtn.setState(ProgressState.None())
|
||||
}
|
||||
|
||||
private val sendSubscriber = SendStateSubscriber(this)
|
||||
private lateinit var keyboardObserver: KeyboardObserver
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
initSendButtonStates()
|
||||
setupAddressOrPayIdLayout()
|
||||
setupAmountLayout()
|
||||
setupFeeLayout()
|
||||
|
|
@ -98,10 +110,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
private fun setupAmountLayout() {
|
||||
store.dispatch(SetMainCurrency(restoreMainCurrency()))
|
||||
store.dispatch(ReceiptAction.RefreshReceipt)
|
||||
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
|
||||
|
||||
tvAmountCurrency.setOnClickListener {
|
||||
store.dispatch(ToggleMainCurrency)
|
||||
store.dispatch(ReceiptAction.RefreshReceipt)
|
||||
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
|
||||
}
|
||||
|
||||
val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) {
|
||||
|
|
@ -229,5 +243,15 @@ class FeeUiHelper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun ToggleWidget.setupSendButtonStateModifiers(context: Context) {
|
||||
mainViewModifiers.clear()
|
||||
mainViewModifiers.add(ReplaceTextStateModifier(context.getString(R.string.send_btn_send), ""))
|
||||
mainViewModifiers.add(
|
||||
TextViewDrawableStateModifier(
|
||||
context.getDrawableCompat(R.drawable.ic_arrow_right), null, TextViewDrawableStateModifier.RIGHT
|
||||
))
|
||||
mainViewModifiers.add(ClickableStateModifier())
|
||||
toggleViewModifiers.clear()
|
||||
toggleViewModifiers.add(ShowHideStateModifier())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ 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.common.text.DecimalDigitsInputFilter
|
||||
import com.tangem.tap.common.toggleWidget.ProgressState
|
||||
import com.tangem.tap.features.send.BaseStoreFragment
|
||||
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
|
|
@ -44,7 +45,22 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
|
|||
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState)
|
||||
}
|
||||
}
|
||||
fg.btnSend.isEnabled = state.sendButtonIsEnabled
|
||||
|
||||
val sendFragment = (fg as? SendFragment) ?: return
|
||||
when (state.sendButtonState) {
|
||||
SendButtonState.ENABLED -> {
|
||||
fg.btnSend.isEnabled = true
|
||||
sendFragment.sendBtn.setState(ProgressState.None(), true)
|
||||
}
|
||||
SendButtonState.DISABLED -> {
|
||||
fg.btnSend.isEnabled = false
|
||||
sendFragment.sendBtn.setState(ProgressState.None(), true)
|
||||
}
|
||||
SendButtonState.PROGRESS -> {
|
||||
fg.btnSend.isEnabled = true
|
||||
sendFragment.sendBtn.setState(ProgressState.Progress(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddressPayIdState(fg: BaseStoreFragment, state: AddressPayIdState) {
|
||||
|
|
@ -101,7 +117,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
|
|||
|
||||
// fg.tvAmountToSendShadow.text = amountToSend
|
||||
// if (amountToSend.length > 10) {
|
||||
// post is needed to wait for text size changes
|
||||
// post is needed to wait for text size changes
|
||||
// fg.tvAmountToSendShadow.post {
|
||||
// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, fg.tvAmountToSendShadow.textSize - 2)
|
||||
// fg.etAmountToSend.update(amountToSend)
|
||||
|
|
|
|||
|
|
@ -87,16 +87,32 @@
|
|||
android:layout_width="100dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSend"
|
||||
style="@style/TapButtonWithIcon"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="end"
|
||||
android:enabled="false"
|
||||
android:fontFamily="@font/saira_semi_condensed_regular"
|
||||
android:text="@string/send_btn_send"
|
||||
app:icon="@drawable/ic_arrow_right" />
|
||||
<FrameLayout
|
||||
android:id="@+id/flSendButtonContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSend"
|
||||
style="@style/TapButtonWithIcon"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="end"
|
||||
android:enabled="false"
|
||||
android:fontFamily="@font/saira_semi_condensed_regular"
|
||||
android:text="@string/send_btn_send"
|
||||
app:icon="@drawable/ic_arrow_right" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_gravity="center"
|
||||
android:elevation="18dp"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="@color/backgroundLightGray" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
<string name="error_payid_already_created">This PayID already exists. Try a different one.</string>
|
||||
<string name="error_creating_payid">Error response while creating PayID.</string>
|
||||
|
||||
<string name="error_unknown">Unknown error</string>
|
||||
<string name="error_payid_verification_failed">PayID verification failed</string>
|
||||
<string name="error_payid_unsupported_by_blockchain">PayID unsupported by blockchain</string>
|
||||
<string name="error_payid_not_registere">PayID not registered</string>
|
||||
|
|
@ -71,5 +72,6 @@
|
|||
<string name="send_total_will_be_sent">will be sent</string>
|
||||
<string name="send_balance">Balance: %1s %2s</string>
|
||||
<string name="send_set_maximum_amount">Maximum amount</string>
|
||||
<string name="send_transaction_complete">Transaction was signed and sent to the blockchain</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue