diff --git a/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt new file mode 100644 index 0000000000..07ac39e6dc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/CurrencyConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common + +import com.tangem.common.extensions.isZero +import java.math.BigDecimal +import java.math.RoundingMode + +/** +[REDACTED_AUTHOR] + */ +class CurrencyConverter(var rateValue: BigDecimal) { + + fun toFiat(crypto: BigDecimal, decimals: Int = 2): BigDecimal { + return rateValue.multiply(crypto).setScale(decimals, RoundingMode.DOWN) + } + + fun toCrypto(fiat: BigDecimal, decimals: Int): BigDecimal { + if (fiat.isZero() || rateValue.isZero()) return fiat + + val scaledRateValue = rateValue.setScale(decimals, RoundingMode.DOWN) + val scaledFiat = fiat.setScale(decimals, RoundingMode.DOWN) + return scaledFiat.divide(scaledRateValue, RoundingMode.UP) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt b/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt new file mode 100644 index 0000000000..69581a7a34 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/KeyboardObserver.kt @@ -0,0 +1,61 @@ +package com.tangem.tap.common + +import android.app.Activity +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import kotlin.math.absoluteValue + +class KeyboardObserver(activity: Activity) { + + private val decorView = activity.window.decorView + private val windowManager = activity.windowManager + private val originalWindowHeight: Int = getWindowHeight() + private val onGlobalLayoutListener: OnGlobalLayoutListener = OnGlobalLayoutListener { onGlobalLayout() } + + private var onKeyboardListener: ((Boolean) -> Unit)? = null + private var lastIsShow = false + private var lastWindowHeight = getWindowHeight() + + fun registerListener(listener: (Boolean) -> Unit) { + decorView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) + onKeyboardListener = listener + } + + fun unregisterListener() { + decorView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener) + onKeyboardListener = null + } + + private fun getWindowHeight() = Rect().apply { decorView.getWindowVisibleDisplayFrame(this) }.bottom + + private fun onGlobalLayout() { + val currentWindowHeight = getWindowHeight() + if (isSoftKeyChanged()) { + lastWindowHeight = currentWindowHeight + return + } + + lastWindowHeight = currentWindowHeight + val isShow = originalWindowHeight != currentWindowHeight + if (lastIsShow == isShow) return + + lastIsShow = isShow + onKeyboardListener?.invoke(isShow) + } + + private fun isSoftKeyChanged() = ((lastWindowHeight - getWindowHeight()).absoluteValue) == getSoftKeyButtonHeight() + + private fun getSoftKeyButtonHeight(): Int { + val applicationDisplayHeight = DisplayMetrics().apply { + windowManager.defaultDisplay.getMetrics(this) + }.heightPixels + + val realDisplayHeight = DisplayMetrics().apply { + windowManager.defaultDisplay.getRealMetrics(this) + }.heightPixels + + return realDisplayHeight - applicationDisplayHeight + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/entities/TapCurrency.kt b/app/src/main/java/com/tangem/tap/common/entities/TapCurrency.kt new file mode 100644 index 0000000000..9e00a33525 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/entities/TapCurrency.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.common.entities + +/** +[REDACTED_AUTHOR] + */ +class TapCurrency { + companion object{ + val main = "USD" + } +} \ No newline at end of file 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 8ecd269976..71f44fadf5 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 @@ -42,7 +42,7 @@ fun BigDecimal.toFormattedString(decimals: Int): String { df.minimumFractionDigits = 0 df.isGroupingUsed = false val bd = BigDecimal(unscaledValue(), scale()) - bd.setScale(decimals, BigDecimal.ROUND_DOWN) + bd.setScale(decimals, RoundingMode.DOWN) return df.format(bd) } @@ -50,4 +50,6 @@ fun BigDecimal.toFiatString(rateValue: BigDecimal): String? { var fiatValue = rateValue.multiply(this) fiatValue = fiatValue.setScale(2, RoundingMode.DOWN) return "≈ USD  $fiatValue" -} \ No newline at end of file +} + +fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() \ 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 new file mode 100644 index 0000000000..a9af6ebb0c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.common.extensions + +import android.view.View +import android.widget.EditText +import android.widget.TextSwitcher +import android.widget.TextView +import com.google.android.material.textfield.TextInputLayout + +/** +[REDACTED_AUTHOR] + */ +fun TextView.update(text: String?) { + if (this.text?.toString() != text) this.text = text +} + +fun EditText.update(text: String?) { + if (this.text?.toString() == text) return + + val textLength = text?.length ?: 0 + + //prevent cursor jumping while editing a text + val cursorPosition = if (selectionEnd > textLength) textLength else selectionEnd + this.setText(text) + if (!isFocused || textLength == 0) return + + if (cursorPosition == 0) setSelection(textLength) + else setSelection(cursorPosition) +} + +fun TextSwitcher.update(text: String?) { + val textView = this.currentView as? TextView ?: return + + if (textView.text?.toString() != text) this.setText(text) +} + +// By default the TextInputLayout didn't activates the error state if the message is empty or null +fun TextInputLayout.enableError(enable: Boolean, errorMessage: String? = null) { + if (enable) { + if (errorMessage == null || errorMessage.isEmpty()) { + error = "Any message" + if (childCount == 2) getChildAt(1).visibility = View.GONE + } else { + error = errorMessage + } + } else { + error = null + isErrorEnabled = false + } +} \ No newline at end of file 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 255ce6949e..c8d2a2c41c 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 @@ -10,7 +10,6 @@ import android.os.Build import android.text.Spannable import android.text.style.ForegroundColorSpan import android.util.TypedValue -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager @@ -132,11 +131,4 @@ fun Context.shareText(text: String) { fun Fragment.shareText(text: String) { requireContext().shareText(text) -} - -fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGroup) { - if (rootView == null) { - val inflatedView = LayoutInflater.from(context).inflate(viewToInflate, rootView) - parent.addView(inflatedView) - } } \ No newline at end of file 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 new file mode 100644 index 0000000000..a4cc3b9e65 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.common.extensions + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.transition.AutoTransition +import androidx.transition.Transition +import androidx.transition.TransitionManager + +/** +[REDACTED_AUTHOR] + */ +fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGroup) { + if (rootView == null) { + val inflatedView = LayoutInflater.from(context).inflate(viewToInflate, rootView) + parent.addView(inflatedView) + } +} + +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/snackBar/MaxAmountSnackbar.kt b/app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt new file mode 100644 index 0000000000..69f2114528 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/snackBar/MaxAmountSnackbar.kt @@ -0,0 +1,81 @@ +package com.tangem.tap.common.snackBar + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.snackbar.BaseTransientBottomBar +import com.google.android.material.snackbar.ContentViewCallback +import com.google.android.material.snackbar.Snackbar +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class MaxAmountSnackbar( + parent: ViewGroup, + content: MaxAmountSnackbarView +) : BaseTransientBottomBar(parent, content, content) { + + companion object { + + fun make(view: View, onClick: () -> Unit): MaxAmountSnackbar { + val parent = view.findSuitableParent() ?: throw IllegalArgumentException( + "No suitable parent found from the given view. Please provide a valid view." + ) + val inflater = LayoutInflater.from(view.context) + val customView = inflater.inflate(R.layout.view_snackbar_max_amount, parent, false) as MaxAmountSnackbarView + customView.setOnClickListener { onClick() } + + return MaxAmountSnackbar(parent, customView).apply { + duration = Snackbar.LENGTH_INDEFINITE + } + } + + private fun View?.findSuitableParent(): ViewGroup? { + var view = this + var fallback: ViewGroup? = null + do { + if (view is CoordinatorLayout) { + return view + } else if (view is FrameLayout) { + if (view.id == android.R.id.content) { + return view + } else { + fallback = view + } + } + + if (view != null) { + val parent = view.parent + view = if (parent is View) parent else null + } + } while (view != null) + return fallback + } + + } +} + +class MaxAmountSnackbarView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), ContentViewCallback { + + init { + View.inflate(context, R.layout.view_snackbar_max_amount_content, this) + clipToPadding = false + } + + + override fun animateContentIn(delay: Int, duration: Int) { + } + + override fun animateContentOut(delay: Int, duration: Int) { + } +} 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 index 1fcaec6111..aae1e5a5b7 100644 --- 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 @@ -7,8 +7,10 @@ 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.SetAddressOrPayId +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 @@ -30,12 +32,23 @@ val sendMiddleware: Middleware = { dispatch, appState -> } private fun handleSendAction(action: Action) { - val sendAction = action as? SendScreenActionUI ?: return + val sendAction = action as? SendScreenActionUi ?: return when (sendAction) { is AddressPayIdActionUi -> { when (sendAction) { - is SetAddressOrPayId -> AddressPayIdHandler().handle(sendAction.data) + 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)) + } + } } } } 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 index 0ec3afea23..5eb886c5dd 100644 --- 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 @@ -1,13 +1,22 @@ 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] @@ -27,12 +36,19 @@ private fun internalReduce(incomingAction: Action, sendState: SendState): SendSt if (incomingAction is ReleaseSendState) return SendState() val action = incomingAction as? SendScreenAction ?: return sendState - return when (action) { + 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( @@ -41,10 +57,10 @@ fun handleAddressPayIdActionUi( state: AddressPayIdState ): SendState { val result = when (action) { - is SetAddressOrPayId -> state + is ChangeAddressOrPayId -> state is SetTruncateHandler -> state.copy(truncateHandler = action.handler) is TruncateOrRestore -> { - if(action.truncate) state.copy(etFieldValue = state.truncatedFieldValue) + if (action.truncate) state.copy(etFieldValue = state.truncatedFieldValue) else state.copy(etFieldValue = state.normalFieldValue) } } @@ -65,6 +81,96 @@ private fun handleAddressPayIdAction( 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 -> { @@ -77,4 +183,4 @@ private fun handleFeeActionUi(action: FeeActionUi, sendState: SendState, state: } private fun updateLastState(sendState: SendState, lastChangedState: StateType): SendState = - sendState.copy(lastChangedStateType = lastChangedState) + 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 d6f5950ea7..7bbb20261e 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 @@ -6,19 +6,13 @@ import org.rekotlin.Action [REDACTED_AUTHOR] */ interface SendScreenAction : Action -interface SendScreenActionUI : SendScreenAction +interface SendScreenActionUi : SendScreenAction object ReleaseSendState : Action -sealed class FeeActionUi : SendScreenActionUI { - object ToggleFeeLayoutVisibility : FeeActionUi() - data class ChangeSelectedFee(val id: Int) : FeeActionUi() - class ChangeIncludeFee(val isChecked: Boolean) : FeeActionUi() -} - -// shortness AddressOrPayId = APid -sealed class AddressPayIdActionUi : SendScreenActionUI { - data class SetAddressOrPayId(val data: String) : AddressPayIdActionUi() +// Address or PayId +sealed class AddressPayIdActionUi : SendScreenActionUi { + data class ChangeAddressOrPayId(val data: String) : AddressPayIdActionUi() data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi() data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi() } @@ -43,4 +37,19 @@ sealed class AddressPayIdVerifyAction : SendScreenAction { data class SetError(val address: String, val reason: FailReason) : AddressVerification() data class SetWalletAddress(val address: String) : AddressVerification() } +} + +// Amount to send +sealed class AmountActionUi : SendScreenActionUi { + object SetMaxAmount : AmountActionUi() + data class ChangeAmountToSend(val data: String) : AmountActionUi() + data class SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi() + object ToggleMainCurrency : AmountActionUi() +} + +// Fee +sealed class FeeActionUi : SendScreenActionUi { + object ToggleFeeLayoutVisibility : FeeActionUi() + data class ChangeSelectedFee(val id: Int) : FeeActionUi() + class ChangeIncludeFee(val isChecked: Boolean) : FeeActionUi() } \ 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 index 4fde717e85..542699ca22 100644 --- 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 @@ -1,18 +1,20 @@ package com.tangem.tap.features.send.redux import android.view.View -import com.tangem.blockchain.common.WalletManager +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 walletManager: WalletManager? = null, val lastChangedStateType: StateType = NoneState(), val addressPayIdState: AddressPayIdState = AddressPayIdState(), - val feeLayoutState: FeeLayoutState = FeeLayoutState() + val amountState: AmountState = AmountState(), + val feeLayoutState: FeeLayoutState = FeeLayoutState(), + val sendButtonIsEnabled: Boolean = false ) : StateType class NoneState : StateType @@ -73,6 +75,24 @@ data class AddressPayIdState( } } +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, 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 ec3e9b7b42..c5cb65e888 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 @@ -1,16 +1,23 @@ package com.tangem.tap.features.send.ui +import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View import android.widget.EditText +import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener +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.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.FeeActionUi.* +import com.tangem.tap.features.send.redux.MainCurrencyType import com.tangem.tap.features.send.redux.ReleaseSendState import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber import com.tangem.tap.mainScope @@ -19,6 +26,7 @@ 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.layout_send_address_payid.* +import kotlinx.android.synthetic.main.layout_send_amount.* import kotlinx.android.synthetic.main.layout_send_network_fee.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose @@ -30,11 +38,13 @@ import kotlinx.coroutines.flow.* class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) + private lateinit var keyboardObserver: KeyboardObserver override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupAddressOrPayIdLayout() + setupAmountLayout() setupFeeLayout() } @@ -47,11 +57,11 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { etAddressOrPayId.inputedTextAsFlow() .debounce(400) .filter { store.state.sendState.addressPayIdState.etFieldValue != it } - .onEach { store.dispatch(SetAddressOrPayId(it)) } + .onEach { store.dispatch(ChangeAddressOrPayId(it)) } .launchIn(mainScope) imvPaste.setOnClickListener { - store.dispatch(SetAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: "")) + store.dispatch(ChangeAddressOrPayId(requireContext().getFromClipboard()?.toString() ?: "")) store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) } imvQrCode.setOnClickListener { @@ -66,10 +76,57 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { if (requestCode != ScanQrCodeActivity.SCAN_QR_REQUEST_CODE) return val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: "" - store.dispatch(SetAddressOrPayId(scannedCode)) + store.dispatch(ChangeAddressOrPayId(scannedCode)) store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused)) } + private fun setupAmountLayout() { + store.dispatch(SetMainCurrency(restoreMainCurrency())) + tvAmountCurrency.setOnClickListener { store.dispatch(ToggleMainCurrency) } + + val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) { store.dispatch(SetMaxAmount) } + var snackbarControlledByChangingFocus = false + keyboardObserver = KeyboardObserver(requireActivity()) + keyboardObserver.registerListener { isShow -> + if (snackbarControlledByChangingFocus) return@registerListener + + if (isShow) { + if (etAmountToSend.isFocused && !maxAmountSnackbar.isShown) maxAmountSnackbar.show() + } else { + if (maxAmountSnackbar.isShown) maxAmountSnackbar.dismiss() + } + } + + etAmountToSend.setOnFocusChangeListener { v, hasFocus -> + snackbarControlledByChangingFocus = true + if (hasFocus) { + etAmountToSend.postDelayed(200) { + maxAmountSnackbar.show() + snackbarControlledByChangingFocus = false + + } + } else { + etAmountToSend.postDelayed(350) { + maxAmountSnackbar.dismiss() + snackbarControlledByChangingFocus = false + } + } + } + + val prevFocusChangeListener = etAmountToSend.onFocusChangeListener + etAmountToSend.setOnFocusChangeListener { v, hasFocus -> + prevFocusChangeListener.onFocusChange(v, hasFocus) + if (hasFocus && etAmountToSend.text?.toString() == "0") etAmountToSend.setText("") + if (!hasFocus && etAmountToSend.text?.toString() == "") etAmountToSend.setText("0") + } + + etAmountToSend.inputedTextAsFlow() + .debounce(400) + .filter { store.state.sendState.amountState.etAmountFieldValue != it } + .onEach { store.dispatch(ChangeAmountToSend(it)) } + .launchIn(mainScope) + } + private fun setupFeeLayout() { flExpandCollapse.setOnClickListener { store.dispatch(ToggleFeeLayoutVisibility) @@ -86,11 +143,24 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { store.subscribe(sendSubscriber) { appState -> appState.skipRepeats { oldState, newState -> oldState == newState }.select { it.sendState } } - storeSubscribersList.add(sendSubscriber) } + fun restoreMainCurrency(): MainCurrencyType { + val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE) + val mainCurrency = sp.getString("mainCurrency", TapCurrency.main) + val foundType = MainCurrencyType.values() + .firstOrNull { it.name.toLowerCase() == mainCurrency!!.toLowerCase() } ?: MainCurrencyType.FIAT + return foundType + } + + fun saveMainCurrency(type: MainCurrencyType) { + val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE) + sp.edit().putString("mainCurrency", type.name).apply() + } + override fun onDestroy() { + keyboardObserver.unregisterListener() store.dispatch(ReleaseSendState) super.onDestroy() } 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 1027616f81..9111bf2703 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,16 +1,23 @@ package com.tangem.tap.features.send.ui.stateSubscribers import android.content.Context +import android.util.TypedValue import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.transition.TransitionManager +import com.tangem.tap.common.extensions.beginDelayedTransition +import com.tangem.tap.common.extensions.enableError +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.ui.SendFragment 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.layout_send_address_payid.* +import kotlinx.android.synthetic.main.layout_send_amount.* import kotlinx.android.synthetic.main.layout_send_network_fee.* /** @@ -22,7 +29,10 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber handleFeeLayoutState(fg, state.feeLayoutState) is AddressPayIdState -> handleAddressPayIdState(fg, state.addressPayIdState) + is AmountState -> handleAmountState(fg, state.amountState) } + + fg.btnSend.isEnabled = state.sendButtonIsEnabled } private fun handleAddressPayIdState(fg: Fragment, state: AddressPayIdState) { @@ -50,16 +60,9 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber 10) { + // 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) + if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length) + } + } else { + val textSize = fg.resources.getDimension(R.dimen.text_size_amount_to_send) + fg.tvAmountToSendShadow.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) + fg.etAmountToSend.update(amountToSend) + if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length) + } + + fg.tvAmountCurrency.update(state.mainCurrency.displayedValue) + (fg as? SendFragment)?.let { it.saveMainCurrency(state.mainCurrency.value) } + + val balanceText = fg.getString(R.string.send_balance, + state.mainCurrency.displayedValue, + state.balance.toPlainString()) + fg.tvBalance.update(balanceText) + } } \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000000..465c50c0cd --- /dev/null +++ b/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000000..a4b3ff52cc --- /dev/null +++ b/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/btn_expand_collapse.xml b/app/src/main/res/layout/btn_expand_collapse.xml index 17ab5b0e2c..8b1bbc2529 100644 --- a/app/src/main/res/layout/btn_expand_collapse.xml +++ b/app/src/main/res/layout/btn_expand_collapse.xml @@ -11,7 +11,6 @@ android:clickable="true" android:focusable="true"> - \ No newline at end of file diff --git a/app/src/main/res/layout/btn_paste.xml b/app/src/main/res/layout/btn_paste.xml index 01c27a3bfc..e561f4477f 100644 --- a/app/src/main/res/layout/btn_paste.xml +++ b/app/src/main/res/layout/btn_paste.xml @@ -10,7 +10,6 @@ android:clickable="true" android:focusable="true"> - + android:layout_marginTop="16dp" /> diff --git a/app/src/main/res/layout/layout_send_address_payid.xml b/app/src/main/res/layout/layout_send_address_payid.xml index 5cb09462f0..16cd313308 100644 --- a/app/src/main/res/layout/layout_send_address_payid.xml +++ b/app/src/main/res/layout/layout_send_address_payid.xml @@ -46,7 +46,7 @@ layout="@layout/btn_qr_code" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="16dp" + android:layout_marginTop="14dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId" /> diff --git a/app/src/main/res/layout/layout_send_amount.xml b/app/src/main/res/layout/layout_send_amount.xml index 92b912df35..09b79ee038 100644 --- a/app/src/main/res/layout/layout_send_amount.xml +++ b/app/src/main/res/layout/layout_send_amount.xml @@ -7,40 +7,70 @@ android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/tilAddressOrPayId"> - - + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:focusable="false" + android:visibility="invisible"> - + + + + + + + + + + + - + app:layout_constraintTop_toBottomOf="@+id/flAmountToSend"> + android:layout_gravity="end" + android:textColor="@color/darkGray1" /> - - - + \ No newline at end of file diff --git a/app/src/main/res/layout/view_snackbar_max_amount.xml b/app/src/main/res/layout/view_snackbar_max_amount.xml new file mode 100644 index 0000000000..141e31aa66 --- /dev/null +++ b/app/src/main/res/layout/view_snackbar_max_amount.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_snackbar_max_amount_content.xml b/app/src/main/res/layout/view_snackbar_max_amount_content.xml new file mode 100644 index 0000000000..9b905b7aab --- /dev/null +++ b/app/src/main/res/layout/view_snackbar_max_amount_content.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 3941e9fb33..c3b19a53d6 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -31,4 +31,6 @@ 4dp + 32sp + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3cd7b67af8..9d8356337d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ Amount Fee will be sent - Balance: + Balance: %1s %2s + Maximum amount \ No newline at end of file