Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-08 15:50:55 +03:00
parent 9b3f6994a9
commit 98ea9b0ead
25 changed files with 648 additions and 88 deletions

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.common.entities
/**
[REDACTED_AUTHOR]
*/
class TapCurrency {
companion object{
val main = "USD"
}
}

View file

@ -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"
}
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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<MaxAmountSnackbar>(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) {
}
}

View file

@ -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<AppState> = { 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))
}
}
}
}
}

View file

@ -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)

View file

@ -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()
}

View file

@ -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<T>(
val value: T,
val displayedValue: String
)
data class AmountState(
val etAmountFieldValue: String = BigDecimal.ZERO.toPlainString(),
val cursorAtTheSamePosition: Boolean = true,
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balance: BigDecimal = BigDecimal.ZERO,
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.main),
val amountIsOverBalance: Boolean = false
) : StateType
data class FeeLayoutState(
val visibility: Int = View.GONE,
val selectedFeeId: Int = R.id.chipNormal,

View file

@ -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()
}

View file

@ -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<SendStat
when (state.lastChangedStateType) {
is FeeLayoutState -> 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<SendStat
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
// prevent cycling
if (state.etFieldValue == null || et.text?.toString() == state.etFieldValue) return
if (state.etFieldValue == null) return
// prevent cursor jumping while editing
if (et.isFocused) {
val prevSelection = et.selectionStart
et.setText(state.etFieldValue)
et.setSelection(prevSelection)
} else {
et.setText(state.etFieldValue)
}
et.update(state.etFieldValue)
}
private fun handleFeeLayoutState(fg: Fragment, layoutState: FeeLayoutState) {
@ -67,7 +70,7 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
val rotationAngle = if (fg.imvExpandCollapse.rotation == 0f) 180f else 0f
fg.imvExpandCollapse.rotation = rotationAngle
(fg.llFeeContainer.parent?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) }
(fg.llFeeContainer.parent?.parent as? ViewGroup)?.beginDelayedTransition()
fg.llFeeContainer.visibility = layoutState.visibility
}
@ -79,4 +82,33 @@ class SendStateSubscriber(fragment: Fragment) : FragmentStateSubscriber<SendStat
fg.chipGroup.check(layoutState.selectedFeeId)
}
}
private fun handleAmountState(fg: Fragment, state: AmountState) {
fg.tilAmountToSend.enableError(state.amountIsOverBalance)
val amountToSend = state.etAmountFieldValue
fg.tvAmountToSendShadow.text = amountToSend
if (amountToSend.length > 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)
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="50%p"
android:toXDelta="0" />
<alpha
android:duration="@android:integer/config_mediumAnimTime"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
</set>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0"
android:toXDelta="50%p" />
<alpha
android:duration="@android:integer/config_mediumAnimTime"
android:fromAlpha="1.0"
android:toAlpha="0.0" />
</set>

View file

@ -11,7 +11,6 @@
android:clickable="true"
android:focusable="true">
<!-- padding was added nice ripple effect -->
<ImageView
android:id="@+id/imvExpandCollapse"
android:layout_width="wrap_content"
@ -19,6 +18,7 @@
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="5dp"
android:rotation="180"
app:srcCompat="@drawable/ic_angle_bracket_up" />
</FrameLayout>

View file

@ -10,7 +10,6 @@
android:clickable="true"
android:focusable="true">
<!-- padding was added nice ripple effect -->
<ImageView
android:id="@+id/imvPaste"
android:layout_width="wrap_content"

View file

@ -29,7 +29,7 @@
layout="@layout/layout_send_address_payid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp" />
android:layout_marginTop="16dp" />
<include
layout="@layout/layout_send_amount"
@ -71,11 +71,13 @@
android:layout_height="1dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSend"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_gravity="end"
android:layout_weight="1"
android:enabled="false"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_btn_send"
app:icon="@drawable/ic_arrow_right" />

View file

@ -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" />

View file

@ -7,40 +7,70 @@
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tilAddressOrPayId">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmount"
<FrameLayout
android:id="@+id/flAmountToSend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:boxBackgroundColor="@android:color/transparent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmount"
<FrameLayout
android:id="@+id/flShadow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-light"
android:inputType="numberDecimal"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:text="0"
android:textSize="32sp"
tools:text="139" />
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:focusable="false"
android:visibility="invisible">
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvAmountToSendShadow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:fontFamily="sans-serif-light"
android:maxLines="1"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:textSize="@dimen/text_size_amount_to_send"
app:autoSizeTextType="uniform" />
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmountToSend"
android:layout_width="match_parent"
android:layout_height="82dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:boxBackgroundColor="@android:color/transparent"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmountToSend"
android:layout_width="match_parent"
android:layout_height="82dp"
android:fontFamily="sans-serif-light"
android:inputType="numberDecimal"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:textSize="@dimen/text_size_amount_to_send"
tools:text="139" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<TextSwitcher
android:id="@+id/tvCurrency"
android:id="@+id/tvAmountCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:inAnimation="@anim/slide_in_up"
android:outAnimation="@anim/slide_out_down"
app:layout_constraintBottom_toBottomOf="@+id/tilAmount"
app:layout_constraintBottom_toBottomOf="@+id/flAmountToSend"
app:layout_constraintEnd_toEndOf="parent">
<TextView
@ -49,7 +79,6 @@
android:layout_gravity="end"
android:drawablePadding="10dp"
android:fontFamily="sans-serif-light"
android:text="usd"
android:textAllCaps="true"
android:textColor="@color/blue"
android:textSize="32sp"
@ -61,7 +90,6 @@
android:layout_gravity="end"
android:drawablePadding="10dp"
android:fontFamily="sans-serif-light"
android:text="btc"
android:textAllCaps="true"
android:textColor="@color/blue"
android:textSize="32sp"
@ -69,39 +97,29 @@
</TextSwitcher>
<LinearLayout
android:id="@+id/balanceContainer"
<TextSwitcher
android:id="@+id/tvBalance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
android:inAnimation="@anim/slide_in_right"
android:outAnimation="@android:anim/slide_out_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tilAmount">
app:layout_constraintTop_toBottomOf="@+id/flAmountToSend">
<TextView
android:id="@+id/tvBalance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="5dp"
android:text="@string/send_balance"
android:layout_gravity="end"
android:textColor="@color/darkGray1" />
<TextView
android:id="@+id/tvBalanceCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="5dp"
android:textColor="@color/darkGray1"
tools:text="USD" />
android:layout_gravity="end"
android:textColor="@color/darkGray1" />
<TextView
android:id="@+id/tvBalanceAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/darkGray1"
tools:text="23.45" />
</LinearLayout>
</TextSwitcher>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.tangem.tap.common.snackBar.MaxAmountSnackbarView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="44dp"
android:padding="8dp" />

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:background="@color/tapButtonColorBlack"
tools:layout_height="44dp"
tools:layout_width="match_parent"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_set_maximum_amount"
android:textColor="#F4F5F6"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</merge>

View file

@ -31,4 +31,6 @@
<dimen name="btn_corner_radius">4dp</dimen>
<dimen name="text_size_amount_to_send">32sp</dimen>
</resources>

View file

@ -58,6 +58,7 @@
<string name="send_total_amount">Amount</string>
<string name="send_total_fee">Fee</string>
<string name="send_total_will_be_sent">will be sent</string>
<string name="send_balance">Balance:</string>
<string name="send_balance">Balance: %1s %2s</string>
<string name="send_set_maximum_amount">Maximum amount</string>
</resources>