Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 19:02:08 +05:00
parent 459db3d0df
commit f43f6299d1
62 changed files with 2 additions and 5914 deletions

View file

@ -32,7 +32,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
@ -82,8 +81,6 @@ interface ApplicationEntryPoint {
fun getWalletsRepository(): WalletsRepository
fun getSendFeatureToggles(): SendFeatureToggles
fun getOneTimeEventFilter(): OneTimeEventFilter
fun getGeneralUserWalletsListManager(): UserWalletsListManager

View file

@ -47,7 +47,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
@ -140,9 +139,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val walletsRepository: WalletsRepository
get() = entryPoint.getWalletsRepository()
private val sendFeatureToggles: SendFeatureToggles
get() = entryPoint.getSendFeatureToggles()
private val oneTimeEventFilter: OneTimeEventFilter
get() = entryPoint.getOneTimeEventFilter()
@ -271,7 +267,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
appThemeModeRepository = appThemeModeRepository,
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,

View file

@ -1,36 +0,0 @@
package com.tangem.tap.common
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.scaleToFiat
import java.math.BigDecimal
import java.math.RoundingMode
/**
[REDACTED_AUTHOR]
*/
class CurrencyConverter(
private val rateValue: BigDecimal,
private val decimals: Int,
) {
private val roundingMode = RoundingMode.HALF_UP
fun toFiat(crypto: BigDecimal, fiatDecimals: Int = 2): BigDecimal {
return toFiatUnscaled(crypto).setScale(fiatDecimals, roundingMode)
}
fun toFiatUnscaled(crypto: BigDecimal): BigDecimal {
return rateValue.multiply(crypto).setScale(decimals, roundingMode)
}
fun toFiatWithPrecision(crypto: BigDecimal): BigDecimal {
return toFiatUnscaled(crypto).scaleToFiat(true)
}
fun toCrypto(fiat: BigDecimal): BigDecimal {
if (fiat.isZero()) return fiat
val fiatValue = fiat.setScale(rateValue.scale(), RoundingMode.UP)
val cryptoValue = fiatValue.divide(rateValue, RoundingMode.UP)
return cryptoValue.setScale(decimals, roundingMode)
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.tap.common
import android.view.View
import android.view.ViewTreeObserver
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class GlobalLayoutStateHandler<T : View>(
private val view: T,
attachImmediately: Boolean = true,
) : ViewTreeObserver.OnGlobalLayoutListener {
var onStateChanged: ((T) -> Unit)? = null
private var isAttached: Boolean = false
init {
if (attachImmediately) attach()
}
private fun attach() {
if (isAttached) {
Timber.d("Already attached")
return
}
isAttached = true
view.viewTreeObserver.addOnGlobalLayoutListener(this)
}
fun detach() {
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
isAttached = false
}
override fun onGlobalLayout() {
onStateChanged?.invoke(view)
}
}

View file

@ -1,60 +0,0 @@
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

@ -22,44 +22,6 @@ sealed class Token(
class ButtonShareAddress : Receive("Button - Share Address")
}
sealed class Send(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : Token("Token / Send", event, params, error) {
class ScreenOpened : Send(event = "Send Screen Opened")
class ButtonPaste : Send(event = "Button - Paste")
class ButtonQRCode : Send(event = "Button - QR Code")
class ButtonSwapCurrency : Send(event = "Button - Swap Currency")
class AddressEntered(sourceType: SourceType, validationResult: ValidationResult) : Send(
event = "Address Entered",
params = mapOf(
"Source" to sourceType.name,
"Validation" to validationResult.name,
),
) {
enum class SourceType {
QRCode, PasteButton, PastePopup
}
enum class ValidationResult {
Success, Fail
}
}
class SelectedCurrency(currency: CurrencyType) : Send(
event = "Selected Currency",
params = mapOf("Type" to currency.value),
) {
enum class CurrencyType(val value: String) {
Token(value = "Token"), AppCurrency(value = "App Currency")
}
}
}
sealed class Topup(
event: String,
params: Map<String, String> = mapOf(),

View file

@ -1,16 +0,0 @@
package com.tangem.tap.common.entities
import com.tangem.tap.features.send.redux.states.ButtonState
open class Button(val enabled: Boolean)
open class IndeterminateProgressButton(
val state: ButtonState,
) : Button(state != ButtonState.DISABLED) {
val progressState: ProgressState
get() = when (state) {
ButtonState.PROGRESS -> ProgressState.Loading
else -> ProgressState.Done
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.common.extensions
import android.graphics.Bitmap
import java.io.ByteArrayOutputStream
@Suppress("MagicNumber")
fun Bitmap.toByteArray(): ByteArray {
val stream = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.JPEG, 20, stream)
return stream.toByteArray()
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common.extensions
import com.tangem.common.extensions.isZero
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@ -25,20 +24,4 @@ fun BigDecimal.toFormattedString(
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
// 0.00 -> 0.00
// 0.00002345 -> 0.00002
// 1.00002345 -> 1.00
// 1.45002345 -> 1.45
fun BigDecimal.scaleToFiat(applyPrecision: Boolean = false): BigDecimal {
if (this.isZero()) return this
val scaledFiat = this.setScale(2, RoundingMode.DOWN)
return if (scaledFiat.isZero() && applyPrecision) this.setPrecision(1) else scaledFiat
}
fun BigDecimal.setPrecision(precision: Int, roundingMode: RoundingMode = RoundingMode.DOWN): BigDecimal {
if (precision == precision() || scale() <= precision) return this
return this.setScale(scale() - precision() + precision, roundingMode)
}
fun BigDecimal.isPositive(): Boolean = this.signum() == 1

View file

@ -1,8 +0,0 @@
package com.tangem.tap.common.extensions
import android.webkit.WebView
fun WebView.stop() {
stopLoading()
pauseTimers()
}

View file

@ -1,72 +0,0 @@
package com.tangem.tap.common.leapfrogWidget
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding
class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) {
private lateinit var twinsCardWidget: TwinsCardWidget
private val binding: TestLeapfrogFragmentBinding by viewBinding(TestLeapfrogFragmentBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
}
@Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrogContainer: FrameLayout = view.findViewById(R.id.leapfrog_views_container)
val leapfrog = LeapfrogWidget(leapfrogContainer)
twinsCardWidget = TwinsCardWidget(leapfrog) { 200f }
binding.btnTwinWelcome.setOnClickListener {
twinsCardWidget.toWelcome()
}
binding.btnTwinToLeapfrog.setOnClickListener {
twinsCardWidget.toLeapfrog()
}
binding.btnTwinActivate.setOnClickListener {
twinsCardWidget.toActivate()
}
binding.btnLpInit.setOnClickListener {
leapfrog.initViews()
}
binding.btnLpUnfold.setOnClickListener {
leapfrog.unfold()
}
binding.btnLpFold.setOnClickListener {
leapfrog.fold()
}
binding.btnLpLeap.setOnClickListener {
leapfrog.leap()
}
binding.btnLpLeapBack.setOnClickListener {
leapfrog.leapBack()
}
}
override fun onStart() {
super.onStart()
leapfrogWidgetState?.let { twinsCardWidget.leapfrogWidget.applyState(it) }
}
override fun onStop() {
super.onStop()
leapfrogWidgetState = twinsCardWidget.leapfrogWidget.getState()
}
}
private var leapfrogWidgetState: LeapfrogWidgetState? = null

View file

@ -1,67 +0,0 @@
package com.tangem.tap.common.recyclerView
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.tangem.sdk.extensions.dpToPx
class SpaceItemDecoration(
private val horizontalSpaceDp: Float,
private val verticalSpaceDp: Float,
) : RecyclerView.ItemDecoration() {
private lateinit var space: Space
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (state.itemCount == 0) return
if (!::space.isInitialized) {
space = Space(
view.dpToPx(horizontalSpaceDp).toInt(),
view.dpToPx(verticalSpaceDp).toInt(),
)
}
outRect.left = space.horizontal
outRect.right = space.horizontal
when (state.itemCount) {
1 -> {
outRect.top = space.vertical
outRect.bottom = space.vertical
}
else -> {
val adapterPosition = parent.getChildAdapterPosition(view)
if (adapterPosition == -1) return
when (adapterPosition) {
0 -> {
// first
outRect.top = space.vertical
outRect.bottom = space.vertical / 2
}
state.itemCount - 1 -> {
// last
outRect.top = space.vertical / 2
outRect.bottom = space.vertical
}
else -> {
// middle
outRect.top = space.vertical / 2
outRect.bottom = space.vertical / 2
}
}
}
}
}
private data class Space(
val horizontal: Int,
val vertical: Int,
)
companion object {
fun all(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, dp)
fun vertical(dp: Float): SpaceItemDecoration = SpaceItemDecoration(0f, dp)
fun horizontal(dp: Float): SpaceItemDecoration = SpaceItemDecoration(dp, 0f)
}
}

View file

@ -10,7 +10,6 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
@ -28,7 +27,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),

View file

@ -25,8 +25,6 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
@ -44,7 +42,6 @@ data class AppState(
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
@ -77,7 +74,6 @@ data class AppState(
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
TwinCardsMiddleware.handler,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,

View file

@ -12,7 +12,6 @@ import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import org.rekotlin.Action
@ -71,8 +70,6 @@ sealed class GlobalAction : Action {
val walletPublicKey: ByteArray,
) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()

View file

@ -12,7 +12,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.network.exchangeServices.BuyExchangeService
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -56,18 +55,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
is GlobalAction.RestoreAppCurrency -> {
restoreAppCurrency()
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
if (it.hideWarning(action.warning)) {
// if (WarningMessagesManager.isAlreadySignedHashesWarning()) {
// // TODO: No appropriate warningMessage identification. Make it better later
// store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
// }
store.dispatch(SendAction.Warnings.Update)
}
}
}
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = action.feedbackData,

View file

@ -1,97 +0,0 @@
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 androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.ContentViewCallback
import com.google.android.material.snackbar.Snackbar
import com.tangem.sdk.extensions.dpToPx
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(parent.context)
val customView = inflater.inflate(R.layout.view_snackbar_max_amount, parent, false) as MaxAmountSnackbarView
customView.setOnClickListener { onClick() }
return MaxAmountSnackbar(parent, customView).apply {
updateBottomMargin()
duration = Snackbar.LENGTH_INDEFINITE
}
}
private fun MaxAmountSnackbar.updateBottomMargin() {
ViewCompat.setOnApplyWindowInsetsListener(this.view) { _, insets ->
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val bottomInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
this.view.updateLayoutParams<CoordinatorLayout.LayoutParams> {
bottomMargin = imeInsets - bottomInsets + context.dpToPx(dp = 8f).toInt()
}
insets
}
}
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

@ -1,60 +0,0 @@
package com.tangem.tap.common.text
import android.text.InputFilter
import android.text.Spanned
import java.util.regex.Pattern
/**
[REDACTED_AUTHOR]
*/
class DecimalDigitsInputFilter(
digitsBeforeDecimal: Int,
digitsAfterDecimal: Int,
private val decimalSeparator: String,
) : InputFilter {
private val pattern: Pattern = Pattern.compile(
"(([1-9]{1}[0-9]{0,${digitsBeforeDecimal - 1}})?||[0]{1})" +
"((\\$decimalSeparator[0-9]{0,$digitsAfterDecimal})?)||(\\$decimalSeparator)?",
)
override fun filter(
source: CharSequence,
sourceStart: Int,
sourceEnd: Int,
destination: Spanned,
destinationStart: Int,
destinationEnd: Int,
): CharSequence? {
val destString = destination.toString()
val prefix = destString.substring(0, destinationStart)
val suffix = destString.substring(destinationEnd, destString.length)
val newDestination = prefix + suffix
val resultPrefix = newDestination.substring(0, destinationStart)
val resultSuffix = newDestination.substring(destinationStart, newDestination.length)
val result = resultPrefix + source.toString() + resultSuffix
return if (pattern.matcher(result).matches()) {
null
} else {
val replacedWithAppropriateDecimalSeparator = setDecimalSeparator(result, decimalSeparator)
if (pattern.matcher(replacedWithAppropriateDecimalSeparator).matches()) {
decimalSeparator
} else {
""
}
}
}
companion object {
fun setDecimalSeparator(value: String, decimalSeparator: String): String {
if (value.contains(decimalSeparator)) return value
return if (decimalSeparator == ".") {
value.replace(",", decimalSeparator)
} else {
value.replace(".", decimalSeparator)
}
}
}
}

View file

@ -1,130 +0,0 @@
package com.tangem.tap.common.text
import android.widget.TextView
import com.tangem.tap.common.extensions.isEven
/**
[REDACTED_AUTHOR]
*/
enum class TruncateType {
START, MIDDLE, END
}
interface Truncate {
fun apply(tv: TextView, text: String, with: String): String
companion object {
fun create(type: TruncateType): Truncate {
return when (type) {
TruncateType.START -> TruncateStart()
TruncateType.MIDDLE -> TruncateMiddle()
TruncateType.END -> TruncateEnd()
}
}
}
}
abstract class BaseTruncate : Truncate {
protected var hasBeenTruncated = false
override fun apply(tv: TextView, text: String, with: String): String {
val roughLength = getRoughFitLength(tv, text)
val fittedText = preciseFitting(tv, roughTruncate(text, roughLength), with)
return if (hasBeenTruncated) attachWith(fittedText, with) else fittedText
}
private fun getRoughFitLength(tv: TextView, text: String): Int {
val existingSpace = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd)
val textWillTakeSpace = tv.paint.measureText(text)
val overSizeRatio: Float = textWillTakeSpace / existingSpace
val maxLengthOfText = text.length / overSizeRatio
if (text.length <= maxLengthOfText) return text.length
return maxLengthOfText.toInt()
}
private fun preciseFitting(tv: TextView, text: String, with: String): String {
if (!hasBeenTruncated) return text
val spaceForText = tv.measuredWidth - (tv.paddingStart + tv.paddingEnd)
var fittedText = text
while (tv.paint.measureText(fittedText + with) > spaceForText) {
fittedText = preciseTruncate(fittedText)
}
return fittedText
}
protected abstract fun roughTruncate(text: String, residualLength: Int): String
protected abstract fun preciseTruncate(text: String): String
protected abstract fun attachWith(text: String, with: String): String
}
class TruncateStart : BaseTruncate() {
override fun roughTruncate(text: String, residualLength: Int): String {
if (text.length <= residualLength) return text
hasBeenTruncated = true
return text.substring(residualLength, text.length)
}
override fun preciseTruncate(text: String): String = text.substring(1, text.length)
override fun attachWith(text: String, with: String): String = with + text
}
class TruncateMiddle : BaseTruncate() {
override fun roughTruncate(text: String, residualLength: Int): String {
if (text.length <= residualLength || residualLength < 0) return text
hasBeenTruncated = true
val halfOfResidualLength = residualLength / 2
val leftSide = text.substring(0, halfOfResidualLength)
val rightSide = text.substring(text.length - halfOfResidualLength, text.length)
return leftSide + rightSide
}
override fun preciseTruncate(text: String): String {
val middlePosition = text.length / 2
return if (text.length.isEven()) {
val leftSide = text.substring(0, middlePosition - 1)
val rightSide = text.substring(middlePosition, text.length)
leftSide + rightSide
} else {
val leftSide = text.substring(0, middlePosition)
val rightSide = text.substring(middlePosition + 1, text.length)
leftSide + rightSide
}
}
override fun attachWith(text: String, with: String): String {
val cuttingPosition = text.length / 2
val leftSide = text.substring(0, cuttingPosition)
val rightSide = text.substring(cuttingPosition, text.length)
return leftSide + with + rightSide
}
}
class TruncateEnd : BaseTruncate() {
override fun roughTruncate(text: String, residualLength: Int): String {
if (text.length <= residualLength) return text
hasBeenTruncated = true
return text.substring(0, residualLength)
}
override fun preciseTruncate(text: String): String = text.substring(0, text.length - 1)
override fun attachWith(text: String, with: String): String = text + with
}
fun TextView.truncateWith(text: String, type: TruncateType, with: String = "..."): String {
val truncate = Truncate.create(type)
return truncate.apply(this, text, with)
}
fun TextView.truncateMiddleWith(text: String, with: String = "..."): String =
this.truncateWith(text, TruncateType.MIDDLE, with)

View file

@ -1,61 +0,0 @@
package com.tangem.tap.common.toggleWidget
import android.graphics.drawable.Drawable
import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
/**
[REDACTED_AUTHOR]
*/
open class IndeterminateProgressButtonWidget(
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.Done,
) : ViewStateWidget {
private var text: CharSequence = button.text
private var icon: Drawable? = button.icon
private var iconGravity: Int? = button.iconGravity
init {
if (initialState != ProgressState.Done) changeState(initialState)
}
var isEnabled: Boolean
get() = button.isEnabled
set(value) {
button.isEnabled = value
}
override val mainView: View = button
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
when (progressState) {
ProgressState.Done, ProgressState.Error -> switchToNone()
ProgressState.Loading -> switchToProgress()
else -> {}
}
}
protected open fun switchToNone() {
button.isClickable = true
button.text = text
button.icon = icon
iconGravity?.let { button.iconGravity = it }
progress.hide()
}
protected open fun switchToProgress() {
button.isClickable = false
button.text = ""
button.icon = null
progress.show()
}
}

View file

@ -22,38 +22,14 @@ sealed class TapError(
object UnknownError : TapError(R.string.send_error_unknown)
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
data class UnsupportedState(
val stateError: String,
) : TapError(R.string.common_custom_string, listOf("Unsupported state: $stateError"))
object NoInternetConnection : TapError(R.string.wallet_notification_no_internet)
object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance)
data class AmountLowerExistentialDeposit(
override val args: List<Any>,
) : TapError(R.string.send_error_minimum_balance_format)
object FeeExceedsBalance : TapError(R.string.send_validation_invalid_fee)
object TotalExceedsBalance : TapError(R.string.send_validation_invalid_total)
object InvalidAmountValue : TapError(R.string.send_validation_invalid_amount)
object InvalidFeeValue : TapError(R.string.send_error_invalid_fee_value)
data class DustAmount(override val args: List<Any>) : TapError(R.string.send_error_dust_amount_format)
object DustChange : TapError(R.string.send_error_dust_change)
sealed class WalletManager {
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
class InternalError(message: String) : CustomError(message)
object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
}
sealed class WalletConnect {
object UnsupportedDapp : TapError(R.string.wallet_connect_error_unsupported_dapp)
object UnsupportedLink : TapError(R.string.wallet_connect_error_failed_to_connect)
}
data class ValidateTransactionErrors(
override val errorList: List<TapError>,
override val builder: (List<String>) -> String,
) : TapError(-1), MultiMessageError
}
sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) {

View file

@ -18,7 +18,6 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.features.main.model.MainScreenState
@ -42,7 +41,6 @@ internal class MainViewModel @Inject constructor(
private val blockchainSDKFactory: BlockchainSDKFactory,
private val userWalletsListManager: UserWalletsListManager,
private val walletManagersFacade: WalletManagersFacade,
private val sendFeatureToggles: SendFeatureToggles,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
private val stakingFeatureToggles: StakingFeatureToggles,
@ -68,7 +66,6 @@ internal class MainViewModel @Inject constructor(
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
updateAppCurrencies()
updateSendFeatureToggle()
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
@ -130,12 +127,6 @@ internal class MainViewModel @Inject constructor(
}
}
private fun updateSendFeatureToggle() {
viewModelScope.launch(dispatchers.main) {
sendFeatureToggles.fetchNewSendEnabled()
}
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

@ -1,215 +0,0 @@
package com.tangem.tap.features.send.redux
import com.tangem.Message
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface SendScreenAction : Action
interface SendScreenActionUi : SendScreenAction
object ReleaseSendState : Action
data class PrepareSendScreen(
val walletManager: WalletManager,
val feePaidCurrency: FeePaidCurrency,
val currency: CryptoCurrency,
val coinAmount: Amount?,
val coinRate: BigDecimal?,
val tokenAmount: Amount? = null,
val tokenRate: BigDecimal? = null,
val feeCurrencyRate: BigDecimal? = null,
val feeCurrencyDecimals: Int = 0,
) : SendScreenAction
// Address
sealed class AddressActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressActionUi()
data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi()
data class CheckClipboard(val data: String?) : AddressActionUi()
data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
data class Prepare(
val blockchain: Blockchain,
val walletAddress: String,
val xrpTag: String?,
) : TransactionExtrasAction()
object Release : TransactionExtrasAction()
@Deprecated("Only in legacy send screen")
sealed class XlmMemo : TransactionExtrasAction() {
// data class ChangeSelectedMemo(val memoType: XlmMemoType) : XlmMemo()
data class HandleUserInput(val data: String) : XlmMemo()
}
@Deprecated("Only in legacy send screen")
sealed class BinanceMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : BinanceMemo()
}
@Deprecated("Only in legacy send screen")
sealed class XrpDestinationTag : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : XrpDestinationTag()
}
@Deprecated("Only in legacy send screen")
sealed class TonMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : TonMemo()
}
@Deprecated("Only in legacy send screen")
sealed class CosmosMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : CosmosMemo()
}
@Deprecated("Only in legacy send screen")
sealed class HederaMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : HederaMemo()
}
@Deprecated("Only in legacy send screen")
sealed class AlgorandMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : AlgorandMemo()
}
}
sealed class AddressVerifyAction : SendScreenAction {
enum class Error {
ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN,
ADDRESS_SAME_AS_WALLET,
}
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction()
sealed class AddressVerification : AddressVerifyAction() {
data class SetAddressError(val error: Error?) : AddressVerification()
data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification()
}
}
// Amount to send
sealed class AmountActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AmountActionUi()
object CheckAmountToSend : AmountActionUi()
object SetMaxAmount : AmountActionUi()
data class SetMainCurrency(val mainCurrency: MainCurrencyType) : AmountActionUi()
object ToggleMainCurrency : AmountActionUi()
}
sealed class AmountAction : SendScreenAction {
data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction()
data class SetAmountError(val error: TapError?) : AmountAction()
data class SetDecimalSeparator(val separator: String) : AmountAction()
data class HideBalance(val hide: Boolean) : AmountAction()
}
// Fee
sealed class FeeActionUi : SendScreenActionUi {
object ToggleControlsVisibility : FeeActionUi()
data class ChangeSelectedFee(val feeType: FeeType) : FeeActionUi()
class ChangeIncludeFee(val isIncluded: Boolean) : FeeActionUi()
}
sealed class FeeAction : SendScreenAction {
object RequestFee : FeeAction()
sealed class FeeCalculation : FeeAction() {
data class SetFeeResult(val fee: TransactionFee) : FeeCalculation()
object ClearResult : FeeCalculation()
}
data class ChangeLayoutVisibility(
val main: Boolean? = null,
val controls: Boolean? = null,
val chipGroup: Boolean? = null,
) : FeeAction()
}
sealed class ReceiptAction : SendScreenAction {
object RefreshReceipt : ReceiptAction()
}
sealed class SendActionUi : SendScreenActionUi {
data class SendAmountToRecipient(val messageForSigner: Message) : SendScreenActionUi
object CheckIfTransactionDataWasProvided : SendScreenActionUi
}
sealed class SendAction : SendScreenAction {
data class ChangeSendButtonState(val state: ButtonState) : SendAction()
object SendSuccess : SendAction(), ToastNotificationAction {
override val messageResource: Int = R.string.send_transaction_success
}
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
data class KaspaWarningDialog(
val maxOutputs: Int,
val maxAmount: BigDecimal,
val onOk: () -> Unit,
) : Dialog()
data class ChiaWarningDialog(
val blockchainName: String,
val maxOutputs: Int,
val maxAmount: BigDecimal,
val onOk: () -> Unit,
) : Dialog()
sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog()
data class BlockchainSdkError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
) : Dialog()
}
data class RequestFeeError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
val onRetry: () -> Unit,
) : Dialog()
object Hide : Dialog()
}
sealed class Warnings : SendAction() {
object Update : SendAction()
data class Set(val warningList: List<WarningMessage>) : SendAction()
}
data class SendSpecificTransaction(
val sendAmount: String,
val destinationAddress: String,
val transactionId: String,
) : SendAction()
}

View file

@ -1,225 +0,0 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.mainScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal class AddressMiddleware {
private val addressValidator = AddressValidator()
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch)
is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch)
else -> return
}
}
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
if (input == sendState.addressState.viewFieldValue.value) return
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
}
private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
}
private fun setAddressAndCheck(
data: String,
sourceType: AddressEntered.SourceType?,
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
dispatch(SetWalletAddress(data, isUserInput))
dispatch(AddressActionUi.CheckAddress(sourceType))
}
private fun verifyAddress(
sourceType: AddressEntered.SourceType?,
appState: AppState?,
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
verifyAddress(
address = address,
walletManager = walletManager,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
private fun verifyAddress(
address: String,
walletManager: WalletManager,
sourceType: AddressEntered.SourceType?,
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val wallet = walletManager.wallet
mainScope.launch {
val failReason = withContext(Dispatchers.IO) {
addressValidator.validateAddress(walletManager, address)
}
if (failReason == null) {
dispatchSuccessValidationActions(
address = address,
wallet = wallet,
sourceType = sourceType,
isUserInput = isUserInput,
dispatch = dispatch,
)
} else {
dispatchFailedValidationActions(
failReason = failReason,
sourceType = sourceType,
dispatch = dispatch,
)
}
}
}
private fun dispatchSuccessValidationActions(
address: String,
wallet: Wallet,
sourceType: AddressEntered.SourceType?,
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val addressSchemeSplit = when (wallet.blockchain) {
Blockchain.BitcoinCash, Blockchain.Kaspa -> listOf(address)
else -> address.split(":")
}
val noSchemeAddress = when (addressSchemeSplit.size) {
1 -> address // no scheme
2 -> { // scheme
if (wallet.blockchain.validateShareScheme(addressSchemeSplit[0])) {
addressSchemeSplit[1]
} else {
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
}
else -> { // invalid URI
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
dispatch(SetAddressError(Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
}
val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query?
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Success,
),
)
}
}
private fun dispatchFailedValidationActions(
failReason: Error,
sourceType: AddressEntered.SourceType?,
dispatch: (Action) -> Unit,
) {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
}
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
private fun String.getQueryParameter(name: String): String? {
return this.substringAfter("?").splitToMap("&", "=")[name]
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val address = input ?: return
val walletManager = appState?.sendState?.walletManager ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
is SetWalletAddress -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true))
}
is SetAddressError -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false))
}
}
}
verifyAddress(
address = address,
walletManager = walletManager,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this
.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.associate { it.first() to it.last() }
}

View file

@ -1,70 +0,0 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.blockchains.near.NearWalletManager
import com.tangem.blockchain.blockchains.near.network.NearAccount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.features.send.redux.AddressVerifyAction
internal class AddressValidator {
suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? {
val blockchain = walletManager.wallet.blockchain
val wallet = walletManager.wallet
return if (blockchain.isNear()) {
validateNearAddress(walletManager, address)
} else {
validateAddress(wallet, address)
}
}
private suspend fun validateNearAddress(
walletManager: WalletManager,
address: String,
): AddressVerifyAction.Error? {
// implicit address validation
if (address.length == NEAR_IMPLICIT_ADDRESS_LENGTH && hexRegex.matches(address)) {
return validateAddress(walletManager.wallet, address)
}
// named address validation
if (address.length in NEAR_MIN_ADDRESS_LENGTH until NEAR_IMPLICIT_ADDRESS_LENGTH &&
nearAddressRegex.matches(address)
) {
val result = (walletManager as? NearWalletManager)?.getAccount(address)
return if (result is Result.Success && result.data is NearAccount.Full) {
null
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
return AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
private fun Blockchain.isNear(): Boolean {
return this == Blockchain.Near || this == Blockchain.NearTestnet
}
private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET
}
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
companion object {
private const val NEAR_MIN_ADDRESS_LENGTH = 2
private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64
private val hexRegex = Regex("^[0-9a-f]+$")
private val nearAddressRegex = Regex("^(([a-z\\d]+[\\-_])*[a-z\\d]+\\.)*([a-z\\d]+[\\-_])*[a-z\\d]+\$")
}
}

View file

@ -1,131 +0,0 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionError
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import org.rekotlin.Action
import java.math.BigDecimal
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class AmountMiddleware {
fun handle(action: AmountActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AmountActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AmountActionUi.CheckAmountToSend -> checkAmountToSend(appState, dispatch)
is AmountActionUi.SetMaxAmount -> setMaxAmount(appState, dispatch)
is AmountActionUi.ToggleMainCurrency -> toggleMainCurrency(appState, dispatch)
is AmountActionUi.SetMainCurrency -> return
}
}
private fun handleUserInput(data: String, appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val amountState = sendState.amountState
var input = amountState.toBigDecimalSeparator(data)
input = if (input == ".") "0.0" else input
val inputValue = when {
input.isEmpty() || input == "0" -> BigDecimal.ZERO
else -> BigDecimal(input)
}
if (inputValue.isZero() && amountState.amountToSendCrypto.isZero()) return
val inputValueCrypto = if (amountState.mainCurrency.type == MainCurrencyType.CRYPTO) {
inputValue
} else {
sendState.convertFiatToExtractCrypto(inputValue)
}
dispatch(AmountAction.SetAmount(inputValueCrypto, true))
dispatch(AmountActionUi.CheckAmountToSend)
dispatch(FeeAction.RequestFee)
}
private fun checkAmountToSend(appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val inputCrypto = sendState.amountState.amountToSendCrypto
if (sendState.amountState.viewAmountValue.value == "0" && inputCrypto.isZero()) {
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
}
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto))
val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee?.amount)
val amountFieldErrors = filterErrorsForAmountField(transactionErrors)
if (amountFieldErrors.isEmpty()) {
dispatch(AmountAction.SetAmountError(null))
} else {
dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountFieldErrors, walletManager)))
}
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
}
private fun setMaxAmount(appState: AppState?, dispatch: (Action) -> Unit) {
val sendState = appState?.sendState ?: return
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
if (sendState.amountState.canIncludeFee()) {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true))
dispatch(FeeActionUi.ChangeIncludeFee(true))
}
dispatch(AmountActionUi.CheckAmountToSend)
if (SendState.isReadyToRequestFee()) dispatch(FeeAction.RequestFee)
}
private fun toggleMainCurrency(appState: AppState?, dispatch: (Action) -> Unit) {
val amountState = appState?.sendState?.amountState ?: return
if (!appState.sendState.coinIsConvertible()) return
val type = if (amountState.mainCurrency.type == MainCurrencyType.FIAT) {
MainCurrencyType.CRYPTO
} else {
MainCurrencyType.FIAT
}
dispatch(AmountActionUi.SetMainCurrency(type))
}
}
private fun filterErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {
when (it) {
TransactionError.AmountExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.FeeExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.TotalExceedsBalance -> {
val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance)
if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it)
showIntoAmountField.remove(TransactionError.AmountLowerExistentialDeposit)
}
else -> showIntoAmountField.add(it)
}
}
showIntoAmountField.remove(TransactionError.TezosSendAll)
return showIntoAmountField
}

View file

@ -1,132 +0,0 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.AmountActionUi
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.features.send.redux.states.SendState
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.DispatchFunction
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class RequestFeeMiddleware {
@Suppress("CyclomaticComplexMethod")
fun handle(appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val scanResponse = appState.globalState.scanResponse ?: return
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
}
val typedAmount = sendState.amountState.amountToExtract ?: return
val destinationAddress = sendState.addressState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)
} else {
walletManager
}
scope.launch {
val feeResult = txSender.getFee(destinationAmount, destinationAddress)
withContext(Dispatchers.Main) {
when (feeResult) {
is Result.Success -> {
val result = feeResult.data
// val result = FeeMock.getFee(walletManager.wallet.blockchain)
dispatch(FeeAction.FeeCalculation.SetFeeResult(result))
when (result) {
is TransactionFee.Single -> {
val fee = result.normal.amount.value ?: BigDecimal.ZERO
if (fee.isZero()) {
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
} else {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false))
}
}
is TransactionFee.Choosable -> {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true))
}
}
}
is Result.Failure -> {
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
if (featureToggles.isLocalLogsEnabled) {
saveBlockchainError(feeResult, destinationAddress, destinationAmount, walletManager)
} else {
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
walletManager = walletManager,
amountToSend = destinationAmount,
feeAmount = null,
destinationAddress = destinationAddress,
)
}
val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext
dispatch(
SendAction.Dialog.RequestFeeError(
error = blockchainSdkError,
scanResponse = scanResponse,
onRetry = { dispatch(FeeAction.RequestFee) },
),
)
}
}
dispatch(AmountActionUi.CheckAmountToSend)
}
}
}
private fun saveBlockchainError(
feeResult: Result.Failure,
destinationAddress: String,
destinationAmount: Amount,
walletManager: WalletManager,
) {
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
error = BlockchainErrorInfo(
errorMessage = (feeResult.error as? BlockchainSdkError)?.customMessage
?: "It isn't BlockchainSdkError",
blockchainId = walletManager.wallet.blockchain.id,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
destinationAddress = destinationAddress,
tokenSymbol = if (destinationAmount.type is AmountType.Token) {
destinationAmount.currencySymbol
} else {
""
},
amount = destinationAmount.value?.stripZeroPlainString() ?: "0",
fee = null,
),
)
}
}

View file

@ -1,455 +0,0 @@
package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchainsdk.utils.minimalAmount
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.demo.DemoTransactionSender
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import com.tangem.wallet.R
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.util.EnumSet
/**
[REDACTED_AUTHOR]
*/
class SendMiddleware {
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(
AddressVerifyAction.AddressVerification.SetWalletAddress(
address = transactionData.destinationAddress,
isUserInput = false,
),
)
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
store.dispatchOnMain(
AmountAction.SetAmount(
transactionData.amount.toBigDecimal(),
false,
),
)
}
}
}
nextDispatch(action)
}
}
}
}
private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient,
appState: AppState?,
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanResponse?.card ?: return
val destinationAddress = sendState.addressState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val fee = sendState.feeState.currentFee ?: return
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend())
val transactionErrors = walletManager.validateTransaction(amountToSend, fee.amount)
when {
transactionErrors.contains(TransactionError.TezosSendAll) -> {
val reduceAmount = walletManager.wallet.blockchain.minimalAmount()
dispatch(
SendAction.Dialog.TezosWarningDialog(
reduceCallback = {
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend)
},
sendAllCallback = {
sendTransaction(
action = action,
walletManager = walletManager,
amountToSend = amountToSend,
fee = fee,
feeType = sendState.feeState.selectedFeeType,
destinationAddress = destinationAddress,
transactionExtras = sendState.transactionExtrasState,
card = card,
externalTransactionData = sendState.externalTransactionData,
mainCurrencyType = sendState.amountState.mainCurrency.type,
dispatch = dispatch,
)
},
reduceAmount,
),
)
}
else -> {
sendTransaction(
action = action,
walletManager = walletManager,
amountToSend = amountToSend,
fee = fee,
feeType = sendState.feeState.selectedFeeType,
destinationAddress = destinationAddress,
transactionExtras = sendState.transactionExtrasState,
card = card,
externalTransactionData = sendState.externalTransactionData,
mainCurrencyType = sendState.amountState.mainCurrency.type,
dispatch = dispatch,
)
}
}
}
@Suppress("LongParameterList", "LongMethod", "ComplexMethod")
private fun sendTransaction(
action: SendActionUi.SendAmountToRecipient,
walletManager: WalletManager,
amountToSend: Amount,
fee: Fee,
feeType: FeeType,
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: CardDTO,
externalTransactionData: ExternalTransactionData?,
mainCurrencyType: MainCurrencyType,
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS))
var txData = walletManager.createTransaction(amountToSend, fee, destinationAddress)
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) }
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
transactionExtras.hederaMemoState?.memo?.let { txData = txData.copy(extras = HederaTransactionExtras(it)) }
transactionExtras.algorandMemoState?.memo?.let { txData = txData.copy(extras = AlgorandTransactionExtras(it)) }
scope.launch {
// TODO: Risky commented this part, unknown logic, need to test if removed
// TODO: [REDACTED_JIRA]
// val updateWalletResult = walletManager.safeUpdate()
// if (updateWalletResult is Result.Failure) {
// withMainContext {
// when (val error = updateWalletResult.error) {
// is TapError -> store.dispatchErrorNotification(error)
// is BlockchainSdkError -> {
// updateFeedbackManagerInfo(
// walletManager = walletManager,
// amountToSend = amountToSend,
// feeAmount = fee.amount,
// destinationAddress = destinationAddress,
// )
// dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error))
// }
// else -> {
// val tapError = if (error.message == null) {
// TapError.UnknownError
// } else {
// TapError.CustomError(error.message!!)
// }
// store.dispatchErrorNotification(tapError)
// }
// }
// dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
// }
// return@launch
// }
val tangemSdk = store.inject(DaggerGraphState::cardSdkConfigRepository).sdk
val linkedTerminalState = tangemSdk.config.linkedTerminal
if (card.isStart2Coin) {
tangemSdk.config.linkedTerminal = false
}
val signer = TangemSigner(
card = card,
tangemSdk = tangemSdk,
initialMessage = action.messageForSigner,
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures,
),
)
}
val sendResult = try {
if (card.isDemoCard()) {
DemoTransactionSender(walletManager).send(txData, signer)
} else {
(walletManager as TransactionSender).send(txData, signer)
}
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
tangemSdk.config.linkedTerminal = linkedTerminalState
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
store.dispatchErrorNotification(TapError.CustomError(ex.localizedMessage ?: "Unknown error"))
}
return@launch
}
withMainContext {
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
tangemSdk.config.linkedTerminal = linkedTerminalState
when (sendResult) {
is Result.Success -> {
dispatch(SendAction.SendSuccess)
if (externalTransactionData != null) {
Analytics.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Sell,
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Send(
blockchain = walletManager.wallet.blockchain.fullName,
token = amountToSend.currencySymbol,
feeType = feeType.convertToAnalyticsFeeType(),
),
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
store.dispatchNavigationAction(AppRouter::pop)
}
}
is Result.Failure -> {
updateFeedbackManagerInfo(
sendResult = sendResult.error,
walletManager = walletManager,
amountToSend = amountToSend,
feeAmount = fee.amount,
destinationAddress = destinationAddress,
)
val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
dispatch(
SendAction.Dialog.SendTransactionFails.CardSdkError(
error = tangemSdkError,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
is BlockchainSdkError.CreateAccountUnderfunded -> {
// from XLM, XRP, Polkadot
dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
is BlockchainSdkError.Kaspa.UtxoAmountError -> {
dispatch(
SendAction.Dialog.KaspaWarningDialog(
maxOutputs = error.maxOutputs,
maxAmount = error.maxAmount,
onOk = {
dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false))
dispatch(AmountActionUi.CheckAmountToSend)
},
),
)
}
is BlockchainSdkError.Chia.UtxoAmountError -> {
dispatch(
SendAction.Dialog.ChiaWarningDialog(
blockchainName = Blockchain.Chia.fullName,
maxOutputs = error.maxOutputs,
maxAmount = error.maxAmount,
onOk = {
dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false))
dispatch(AmountActionUi.CheckAmountToSend)
},
),
)
}
else -> {
when {
error.customMessage.contains(DemoTransactionSender.ID) -> {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_done,
messageId = R.string.alert_demo_feature_disabled,
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
),
)
}
else -> {
dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}
}
}
}
}
}
}
}
}
private fun getMemoType(transactionExtras: TransactionExtrasState): Basic.TransactionSent.MemoType {
return when {
transactionExtras.isEmpty() -> Basic.TransactionSent.MemoType.Empty
transactionExtras.isNull() -> Basic.TransactionSent.MemoType.Null
else -> Basic.TransactionSent.MemoType.Full
}
}
private fun Analytics.sendSelectedCurrencyEvent(mainCurrencyType: MainCurrencyType) {
send(
Token.Send.SelectedCurrency(
currency = when (mainCurrencyType) {
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
MainCurrencyType.CRYPTO -> CurrencyType.Token
},
),
)
}
private fun updateFeedbackManagerInfo(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
sendResult: BlockchainError,
) {
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
if (featureToggles.isLocalLogsEnabled) {
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
error = BlockchainErrorInfo(
errorMessage = (sendResult as? BlockchainSdkError)?.customMessage ?: "It isn't BlockchainSdkError",
blockchainId = walletManager.wallet.blockchain.id,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
destinationAddress = destinationAddress,
tokenSymbol = if (amountToSend.type is AmountType.Token) {
amountToSend.currencySymbol
} else {
""
},
amount = amountToSend.value?.stripZeroPlainString() ?: "0",
fee = feeAmount.value?.stripZeroPlainString() ?: "0",
),
)
} else {
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
walletManager = walletManager,
amountToSend = amountToSend,
feeAmount = feeAmount,
destinationAddress = destinationAddress,
)
}
}
fun createValidateTransactionError(
errorList: EnumSet<TransactionError>,
walletManager: WalletManager,
): TapError.ValidateTransactionErrors {
val tapErrors = errorList.map {
when (it) {
TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance
TransactionError.AmountLowerExistentialDeposit -> {
if (walletManager is ExistentialDepositProvider) {
val args = listOf(walletManager.getExistentialDeposit().stripZeroPlainString())
TapError.AmountLowerExistentialDeposit(args)
} else {
TapError.UnknownError
}
}
TransactionError.FeeExceedsBalance -> TapError.FeeExceedsBalance
TransactionError.TotalExceedsBalance -> TapError.TotalExceedsBalance
TransactionError.InvalidAmountValue -> TapError.InvalidAmountValue
TransactionError.InvalidFeeValue -> TapError.InvalidFeeValue
TransactionError.DustAmount -> {
val args = listOf(walletManager.dustValue?.stripZeroPlainString() ?: "0")
TapError.DustAmount(args)
}
TransactionError.DustChange -> TapError.DustChange
else -> TapError.UnknownError
}
}
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return
val warnings = warningsManager.getWarnings(WarningMessage.Location.SendScreen, listOf(blockchain))
dispatch(SendAction.Warnings.Set(warnings))
}

View file

@ -1,52 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AddressVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState)
is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState)
else -> sendState
}
private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressActionUi.HandleUserInput -> state
is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressActionUi.PasteAddress -> return sendState
is AddressActionUi.CheckClipboard -> return sendState
is AddressActionUi.CheckAddress -> return sendState
}
return updateLastState(sendState.copy(addressState = result), result)
}
private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressState = result), result)
}
}

View file

@ -1,99 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.utils.StringsSigns.STARS
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class AmountReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AmountActionUi -> handleUiAction(action, sendState, sendState.amountState)
is AmountAction -> handleAction(action, sendState, sendState.amountState)
else -> sendState
}
private fun handleUiAction(action: AmountActionUi, sendState: SendState, state: AmountState): SendState {
val result = when (action) {
is SetMainCurrency -> {
val currencyCanBeSwitched = sendState.mainCurrencyCanBeSwitched()
when (val currency = if (currencyCanBeSwitched) action.mainCurrency else MainCurrencyType.CRYPTO) {
MainCurrencyType.FIAT -> {
val fiatToSend = if (state.amountToSendCrypto.isZero()) {
BigDecimal.ZERO
} else {
sendState.convertExtractCryptoToFiat(state.amountToSendCrypto)
}
val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString())
state.copy(
viewAmountValue = InputViewValue(viewValue),
viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrency(currency, true),
maxLengthOfAmount = sendState.getDecimals(currency),
cursorAtTheSamePosition = false,
)
}
MainCurrencyType.CRYPTO -> {
val viewValue = state.restoreDecimalSeparator(state.amountToSendCrypto.stripZeroPlainString())
state.copy(
viewAmountValue = InputViewValue(viewValue),
viewBalanceValue = state.balanceCrypto.stripZeroPlainString(),
mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched),
maxLengthOfAmount = sendState.getDecimals(currency),
cursorAtTheSamePosition = false,
)
}
}
}
else -> return sendState
}
return updateLastState(sendState.copy(amountState = result), result)
}
private fun handleAction(action: AmountAction, sendState: SendState, state: AmountState): SendState {
val result = when (action) {
is AmountAction.SetAmount -> {
val amount = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) {
action.amountCrypto
} else {
sendState.convertExtractCryptoToFiat(action.amountCrypto, true)
}
val viewValue = state.restoreDecimalSeparator(amount.stripZeroPlainString())
state.copy(
viewAmountValue = InputViewValue(viewValue, action.isUserInput),
amountToSendCrypto = action.amountCrypto,
cursorAtTheSamePosition = true,
error = null,
)
}
is AmountAction.SetAmountError -> state.copy(error = action.error)
is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator)
is AmountAction.HideBalance -> {
val rescaledBalance = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) {
state.balanceCrypto
} else {
sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
}
state.copy(
hideBalance = action.hide,
viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(),
)
}
}
return updateLastState(sendState.copy(amountState = result), result)
}
}

View file

@ -1,122 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class FeeReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState)
is FeeAction -> handleAction(action, sendState, sendState.feeState)
else -> sendState
}
private fun handleUiAction(action: FeeActionUi, sendState: SendState, state: FeeState): SendState {
val result = when (action) {
is FeeActionUi.ToggleControlsVisibility -> {
state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible)
}
is FeeActionUi.ChangeSelectedFee -> {
val currentFee = state.fees?.let {
createValueOfFeeAmount(action.feeType, it)
}
state.copy(
selectedFeeType = action.feeType,
currentFee = currentFee,
)
}
is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded)
}
return updateLastState(sendState.copy(feeState = result), result)
}
private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState {
val result = when (action) {
is FeeAction.RequestFee -> {
state.copy(progressState = ProgressState.Loading)
}
is FeeAction.ChangeLayoutVisibility -> {
fun getVisibility(current: Boolean, proposed: Boolean?): Boolean = proposed ?: current
state.copy(
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup),
)
}
is FeeAction.FeeCalculation.SetFeeResult -> {
when (val fees = action.fee) {
is TransactionFee.Single -> {
val feeType = FeeType.SINGLE
val currentFee = createValueOfFeeAmount(feeType, fees)
state.copy(
selectedFeeType = feeType,
fees = fees,
currentFee = currentFee,
feeIsApproximate = isFeeApproximate(sendState),
)
}
is TransactionFee.Choosable -> {
val feeType = getCurrentFeeType(state)
val currentFee = createValueOfFeeAmount(feeType, fees)
state.copy(
selectedFeeType = feeType,
fees = fees,
currentFee = currentFee,
feeIsApproximate = isFeeApproximate(sendState),
)
}
}.copy(
progressState = ProgressState.Done,
)
}
FeeAction.FeeCalculation.ClearResult -> {
state.copy(
fees = null,
currentFee = null,
progressState = ProgressState.Done,
)
}
}
return updateLastState(sendState.copy(feeState = result), result)
}
private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Fee {
return when (transactionFee) {
is TransactionFee.Single -> {
transactionFee.normal
}
is TransactionFee.Choosable -> {
when (feeType) {
FeeType.SINGLE -> transactionFee.normal
FeeType.LOW -> transactionFee.minimum
FeeType.NORMAL -> transactionFee.normal
FeeType.PRIORITY -> transactionFee.priority
}
}
}
}
private fun isFeeApproximate(sendState: SendState): Boolean {
val blockchain = sendState.walletManager?.wallet?.blockchain ?: return false
val amountType = sendState.amountState.typeOfAmount
return blockchain.isFeeApproximate(amountType)
}
private fun getCurrentFeeType(state: FeeState): FeeType {
return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType
}
}

View file

@ -1,408 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.Wallet
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.tap.common.extensions.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.store
import com.tangem.utils.StringsSigns.LOWER_SIGN
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
class ReceiptReducer : SendInternalReducer {
private lateinit var sendState: SendState
private lateinit var amountState: AmountState
private lateinit var feeState: FeeState
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
this.sendState = sendState
this.amountState = sendState.amountState
this.feeState = sendState.feeState
return when (action) {
is RefreshReceipt -> handleRefresh(sendState.receiptState)
else -> sendState
}
}
private fun handleRefresh(state: ReceiptState): SendState {
val wallet = sendState.walletManager?.wallet ?: return sendState
val feePaidCurrency = wallet.blockchain.feePaidCurrency()
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount, feePaidCurrency)
val symbols = determineSymbols(wallet, amountState.typeOfAmount, feePaidCurrency)
val showBlank = !SendState.isReadyToSend()
val result = state.copy(
visibleTypeOfReceipt = layoutType,
mainCurrency = amountState.mainCurrency,
fiat = createFiatType(symbols, showBlank),
crypto = createCryptoType(symbols, showBlank),
tokenFiat = createTokenFiatType(symbols, showBlank),
tokenCrypto = createTokenCryptoType(symbols, showBlank),
customTokenFiat = createCustomTokenFiat(symbols, showBlank),
customTokenCrypto = createCustomTokenCrypto(symbols, showBlank),
sameCurrencyFiat = createSameCurrencyFiat(symbols, showBlank),
sameCurrencyCrypto = createSameCurrencyCrypto(symbols, showBlank),
)
return updateLastState(sendState.copy(receiptState = result), result)
}
private fun createFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat {
val feeCrypto = feeState.getCurrentFeeValue()
val feeFiat = convertToFiatPrecision(feeCrypto)
if (showBlank) {
return ReceiptFiat("0", feeFiat, "0", "0", symbols)
}
return if (feeState.feeIsIncluded) {
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto))
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
symbols = symbols,
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
val totalFiat = convertToFiatPrecision(totalAmountCrypto)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun createCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto {
val feeCrypto = feeState.getCurrentFeeValue()
val feeFiat = convertToFiatPrecision(feeCrypto)
if (showBlank) {
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
}
if (feeState.feeIsIncluded) {
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto),
symbols = symbols,
)
} else {
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(totalCrypto),
symbols = symbols,
)
}
}
private fun createTokenFiatType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat {
val feeCoin = feeState.getCurrentFeeValue()
if (showBlank) {
val feeFiat = convertToFiatPrecision(feeCoin)
return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) {
val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin)
val amountFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend)
val totalFiat = amountFiat.plus(feeFiat)
ReceiptTokenFiat(
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenFiat(
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun createTokenCryptoType(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto {
val feeCoin = feeState.getCurrentFeeValue()
if (showBlank) {
return ReceiptTokenCrypto("0", feeCoin.stripZeroPlainString(), "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
return if (sendState.coinIsConvertible() && sendState.tokenIsConvertible()) {
val tokenFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend)
val feeFiat = sendState.coinConverter!!.toFiatUnscaled(feeCoin)
val totalFiat = tokenFiat.plus(feeFiat)
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
symbols = symbols,
)
}
}
private fun createCustomTokenCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenCrypto {
val feeValue = feeState.getCurrentFeeValue()
if (showBlank) {
return ReceiptTokenCrypto("0", feeValue.stripZeroPlainString(), "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
val amountType = amountState.typeOfAmount
val currencyConverter = when {
amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter
amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter
else -> null
}
return if (currencyConverter != null) {
val currencyFiat = currencyConverter.toFiatUnscaled(tokensToSend)
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeValue)
val totalFiat = currencyFiat.plus(feeFiat)
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeValue.stripZeroPlainString().addPrecisionSign(),
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
symbols = symbols,
)
}
}
private fun createCustomTokenFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptTokenFiat {
val feeCoin = feeState.getCurrentFeeValue()
if (showBlank) {
val feeFiat = convertToFiatPrecision(feeCoin)
return ReceiptTokenFiat("0", feeFiat, "0", "0", "0", symbols)
}
val tokensToSend = amountState.amountToSendCrypto
val amountType = amountState.typeOfAmount
val currencyConverter = when {
amountType is AmountType.Token && sendState.feeIsConvertible() -> sendState.tokenConverter
amountType is AmountType.Coin && sendState.feeIsConvertible() -> sendState.coinConverter
else -> null
}
return if (currencyConverter != null) {
val feeFiat = sendState.customFeeConverter!!.toFiatUnscaled(feeCoin)
val amountFiat = currencyConverter.toFiatUnscaled(tokensToSend)
val totalFiat = amountFiat.plus(feeFiat)
ReceiptTokenFiat(
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString().addPrecisionSign(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.scaleToFiat(true).stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
} else {
ReceiptTokenFiat(
amountFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
feeFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
totalFiat = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun createSameCurrencyCrypto(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptCrypto {
val feeCrypto = feeState.getCurrentFeeValue()
val feeFiat = convertToFiatPrecision(feeCrypto)
if (showBlank) {
return ReceiptCrypto("0", feeCrypto.stripZeroPlainString(), "0", "0", "0", symbols)
}
val isToken = amountState.typeOfAmount is AmountType.Token
if (feeState.feeIsIncluded) {
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(value = amountState.amountToSendCrypto, isToken = isToken),
symbols = symbols,
)
} else {
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString().addPrecisionSign(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(value = totalCrypto, isToken = isToken),
symbols = symbols,
)
}
}
private fun createSameCurrencyFiat(symbols: ReceiptSymbols, showBlank: Boolean): ReceiptFiat {
val feeCrypto = feeState.getCurrentFeeValue()
val isToken = amountState.typeOfAmount is AmountType.Token
val feeFiat = convertToFiatPrecision(feeCrypto, isToken = isToken)
if (showBlank) {
return ReceiptFiat("0", feeFiat, "0", "0", symbols)
}
return if (feeState.feeIsIncluded) {
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto), isToken = isToken)
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols,
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto, isToken = isToken)
val totalFiat = convertToFiatPrecision(totalAmountCrypto, isToken = isToken)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.scaleToFiat().stripZeroPlainString(),
symbols = symbols,
)
}
}
private fun determineSymbols(
wallet: Wallet,
amountType: AmountType,
feePaidCurrency: FeePaidCurrency,
): ReceiptSymbols {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency.code,
crypto = wallet.blockchain.currency,
token = when (amountType) {
is AmountType.Token -> amountType.token.symbol
else -> null
},
fee = when (feePaidCurrency) {
FeePaidCurrency.Coin -> wallet.blockchain.currency
FeePaidCurrency.SameCurrency -> store.state.sendState.currency?.symbol
is FeePaidCurrency.Token -> feePaidCurrency.token.symbol
is FeePaidCurrency.FeeResource -> feePaidCurrency.currency
},
)
}
private fun determineLayoutType(
mainCurrencyType: MainCurrencyType,
amountType: AmountType,
feePaidCurrency: FeePaidCurrency,
): ReceiptLayoutType {
return when (mainCurrencyType) {
MainCurrencyType.FIAT -> determineFiatLayoutType(feePaidCurrency, amountType)
MainCurrencyType.CRYPTO -> determineCryptoLayoutType(feePaidCurrency, amountType)
}
}
private fun determineFiatLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> {
val amountToken = (amountType as? AmountType.Token)?.token
val sameToken =
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
if (sameToken) ReceiptLayoutType.SAME_CURRENCY_FIAT else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT
}
FeePaidCurrency.Coin -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.FIAT
is AmountType.Token -> ReceiptLayoutType.TOKEN_FIAT
is AmountType.FeeResource,
AmountType.Reserve,
-> ReceiptLayoutType.UNKNOWN
}
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY_FIAT
is FeePaidCurrency.FeeResource -> ReceiptLayoutType.SAME_CURRENCY
}
}
private fun determineCryptoLayoutType(feePaidCurrency: FeePaidCurrency, amountType: AmountType): ReceiptLayoutType {
return when (feePaidCurrency) {
is FeePaidCurrency.Token -> {
val amountToken = (amountType as? AmountType.Token)?.token
val sameToken =
feePaidCurrency.token.contractAddress.equals(amountToken?.contractAddress, ignoreCase = true)
if (sameToken) ReceiptLayoutType.SAME_CURRENCY else ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN
}
FeePaidCurrency.SameCurrency -> ReceiptLayoutType.SAME_CURRENCY
FeePaidCurrency.Coin -> when (amountType) {
AmountType.Coin -> ReceiptLayoutType.CRYPTO
is AmountType.Token -> ReceiptLayoutType.TOKEN_CRYPTO
is AmountType.FeeResource,
AmountType.Reserve,
-> ReceiptLayoutType.UNKNOWN
}
is FeePaidCurrency.FeeResource -> ReceiptLayoutType.UNKNOWN
}
}
private fun convertToFiatPrecision(value: BigDecimal, isToken: Boolean = false): String {
return when {
!isToken && sendState.coinIsConvertible() -> {
sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
}
isToken && sendState.tokenIsConvertible() -> {
sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
}
else -> {
BigDecimalFormatter.EMPTY_BALANCE_SIGN
}
}
}
private fun String.addPrecisionSign(): String {
val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this
return result.trim()
}
}

View file

@ -1,133 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.IdStateHolder
import com.tangem.tap.features.send.redux.states.SendState
import org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface SendInternalReducer {
fun handle(action: SendScreenAction, sendState: SendState): SendState
}
object SendScreenReducer {
fun reduce(incomingAction: Action, sendState: SendState): SendState {
if (incomingAction is ReleaseSendState) return SendState()
val action = incomingAction as? SendScreenAction ?: return sendState
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressActionUi, is AddressVerifyAction -> AddressReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
is ReceiptAction -> ReceiptReducer()
is SendAction -> SendReducer()
else -> EmptyReducer()
}
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 = IndeterminateProgressButton(action.state))
}
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.KaspaWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.ChiaWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState)
is SendAction.SendSuccess -> sendState.copy(isSuccessSend = true)
else -> return sendState
}
return updateLastState(result, result)
}
private fun handleSendSpecificTransactionAction(
action: SendAction.SendSpecificTransaction,
state: SendState,
): SendState {
return state.copy(
externalTransactionData =
ExternalTransactionData(action.sendAmount, action.destinationAddress, action.transactionId),
feeState = state.feeState.copy(
includeFeeSwitcherIsEnabled = false,
),
amountState = state.amountState.copy(
inputIsEnabled = false,
),
addressState = state.addressState.copy(
inputIsEnabled = false,
),
)
}
}
private class EmptyReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = sendState
}
private class PrepareSendScreenStatesReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val prepareAction = action as PrepareSendScreen
val walletManager = action.walletManager
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
val canIncludeFee = canIncludeFee(
typeOfAmount = amountToExtract.type,
feePaidCurrency = action.feePaidCurrency,
)
return sendState.copy(
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
customFeeConverter = action.feeCurrencyRate?.let { CurrencyConverter(it, action.feeCurrencyDecimals) },
amountState = sendState.amountState.copy(
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO,
),
feeState = sendState.feeState.copy(includeFeeSwitcherIsEnabled = canIncludeFee),
canIncludeFee = canIncludeFee,
currency = action.currency,
)
}
private fun canIncludeFee(typeOfAmount: AmountType, feePaidCurrency: FeePaidCurrency): Boolean {
return when (feePaidCurrency) {
FeePaidCurrency.Coin -> typeOfAmount == AmountType.Coin
FeePaidCurrency.SameCurrency -> true
is FeePaidCurrency.Token -> {
val sendToken = (typeOfAmount as? AmountType.Token)?.token ?: return false
sendToken.contractAddress.equals(feePaidCurrency.token.contractAddress, ignoreCase = true) &&
sendToken.name.equals(feePaidCurrency.token.name, ignoreCase = true) &&
sendToken.symbol.equals(feePaidCurrency.token.symbol, ignoreCase = true)
}
is FeePaidCurrency.FeeResource -> false
}
}
}
internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState {
sendState.lastChangedStates.add(lastChangedState.stateId)
return sendState
}

View file

@ -1,201 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction.*
import com.tangem.tap.features.send.redux.states.*
/**
[REDACTED_AUTHOR]
*/
class TransactionExtrasReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
return when (action) {
is Prepare -> handleInitialization(action, sendState)
Release -> handleRelease(sendState)
is XlmMemo -> handleXlmMemo(action, sendState, sendState.transactionExtrasState)
is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState)
is HederaMemo -> handleHederaMemo(action, sendState, sendState.transactionExtrasState)
is AlgorandMemo -> handleAlgorandMemo(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
}
private fun handleInitialization(action: Prepare, sendState: SendState): SendState {
val emptyResult = TransactionExtrasState()
val result = when (action.blockchain) {
Blockchain.XRP -> {
val address = action.walletAddress.substringAfter(":")
// 'r' - without tag, 'x' - with tag
if (address.startsWith("r", true)) {
val tag = action.xrpTag?.toLongOrNull()
if (tag == null) {
TransactionExtrasState(xrpDestinationTag = XrpDestinationTagState())
} else {
TransactionExtrasState(
xrpDestinationTag = XrpDestinationTagState(
viewFieldValue = InputViewValue("$tag", false),
tag = tag,
),
)
}
} else {
emptyResult
}
}
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState())
Blockchain.TON, Blockchain.TONTestnet -> TransactionExtrasState(tonMemoState = TonMemoState())
Blockchain.Cosmos,
Blockchain.TerraV1,
Blockchain.TerraV2,
-> TransactionExtrasState(cosmosMemoState = CosmosMemoState())
Blockchain.Hedera, Blockchain.HederaTestnet -> TransactionExtrasState(hederaMemoState = HederaMemoState())
Blockchain.Algorand, Blockchain.AlgorandTestnet -> TransactionExtrasState(
algorandMemoState = AlgorandMemoState(),
)
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleRelease(sendState: SendState): SendState {
val result = TransactionExtrasState()
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleXlmMemo(action: XlmMemo, sendState: SendState, infoState: TransactionExtrasState): SendState {
fun clearMemo(memo: XlmMemoState): XlmMemoState = memo.copy(text = null, id = null, error = null)
val result = when (action) {
is XlmMemo.HandleUserInput -> {
val inputViewValue = InputViewValue(action.data, true)
var memo = infoState.xlmMemo?.copy(viewFieldValue = inputViewValue)
?: XlmMemoState(inputViewValue)
memo = clearMemo(memo)
memo = when (memo.selectedMemoType) {
XlmMemoType.TEXT -> {
if (XlmMemoState.isAssignableValue(action.data)) {
memo.copy(text = StellarMemo.Text(action.data))
} else {
memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO)
}
}
XlmMemoType.ID -> {
if (XlmMemoState.isAssignableValue(action.data)) {
memo.copy(id = StellarMemo.Id(action.data.toBigInteger()))
} else {
memo.copy(error = TransactionExtraError.INVALID_XLM_MEMO)
}
}
}
infoState.copy(xlmMemo = memo)
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleBinanceMemo(
action: BinanceMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is BinanceMemo.HandleUserInput -> {
val tag = action.data.toBigIntegerOrNull()
if (tag != null) {
val input = InputViewValue(action.data, true)
val tagState = BinanceMemoState(input, tag)
infoState.copy(binanceMemo = tagState)
} else {
infoState
}
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleXrpTag(
action: XrpDestinationTag,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is XrpDestinationTag.HandleUserInput -> {
val tag = action.data.toLongOrNull()
if (tag != null) {
val input = InputViewValue(action.data, true)
val tagState = if (tag <= XrpDestinationTagState.MAX_NUMBER) {
XrpDestinationTagState(input, tag)
} else {
XrpDestinationTagState(input, error = TransactionExtraError.INVALID_DESTINATION_TAG)
}
infoState.copy(xrpDestinationTag = tagState)
} else {
infoState
}
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleTonMemo(action: TonMemo, sendState: SendState, infoState: TransactionExtrasState): SendState {
val result = when (action) {
is TonMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(tonMemoState = TonMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleCosmosMemo(
action: CosmosMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is CosmosMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(cosmosMemoState = CosmosMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleHederaMemo(
action: HederaMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is HederaMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(hederaMemoState = HederaMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleAlgorandMemo(
action: AlgorandMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is AlgorandMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(algorandMemoState = AlgorandMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
}

View file

@ -1,150 +0,0 @@
package com.tangem.tap.features.send.redux.states
import androidx.core.text.isDigitsOnly
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressVerifyAction
import java.math.BigInteger
data class AddressState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
override val stateId: StateId = StateId.ADDRESS_PAY_ID
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
}
@Deprecated("Legacy")
data class TransactionExtrasState(
val xlmMemo: XlmMemoState? = null,
val binanceMemo: BinanceMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null,
val tonMemoState: TonMemoState? = null,
val cosmosMemoState: CosmosMemoState? = null,
val hederaMemoState: HederaMemoState? = null,
val algorandMemoState: AlgorandMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
fun isNull(): Boolean {
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
cosmosMemoState == null && hederaMemoState == null && algorandMemoState == null
}
fun isEmpty(): Boolean {
val isXlmEmpty = xlmMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isBinanceEmpty = binanceMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isHederaEmpty = hederaMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isAlgorandEmpty = algorandMemoState?.viewFieldValue?.value?.isEmpty() ?: false
return isXlmEmpty ||
isBinanceEmpty ||
isXrpEmpty ||
isTonEmpty ||
isCosmosEmpty ||
isHederaEmpty ||
isAlgorandEmpty
}
}
enum class XlmMemoType {
TEXT, ID
}
data class XlmMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val text: StellarMemo.Text? = null,
val id: StellarMemo.Id? = null,
val error: TransactionExtraError? = null,
) {
val memo: StellarMemo?
get() = when (selectedMemoType) {
XlmMemoType.TEXT -> text
XlmMemoType.ID -> id
}
val selectedMemoType: XlmMemoType
get() = determineMemoType(viewFieldValue.value)
companion object {
fun determineMemoType(value: String): XlmMemoType = when {
value.isNotEmpty() && value.isDigitsOnly() -> XlmMemoType.ID
else -> XlmMemoType.TEXT
}
@Suppress("MagicNumber")
fun isAssignableValue(value: String): Boolean = when (determineMemoType(value)) {
XlmMemoType.TEXT -> {
// from org.stellar.sdk.MemoText
value.toByteArray().size <= 28
}
XlmMemoType.ID -> {
try {
// from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo
value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger()
} catch (ex: NumberFormatException) {
false
}
}
}
}
}
data class BinanceMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: BigInteger? = null,
val error: TransactionExtraError? = null,
)
// tag must contains only digits
data class XrpDestinationTagState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val tag: Long? = null,
val error: TransactionExtraError? = null,
) {
companion object {
const val MAX_NUMBER: Long = 4294967295
}
}
data class TonMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class CosmosMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class HederaMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class AlgorandMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
enum class TransactionExtraError {
INVALID_DESTINATION_TAG,
INVALID_XLM_MEMO,
INVALID_BINANCE_MEMO,
}

View file

@ -1,43 +0,0 @@
package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.tap.common.entities.ProgressState
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
enum class FeeType {
SINGLE, LOW, NORMAL, PRIORITY;
}
fun FeeType.convertToAnalyticsFeeType(): AnalyticsParam.FeeType {
return when (this) {
FeeType.SINGLE -> AnalyticsParam.FeeType.Fixed
FeeType.LOW -> AnalyticsParam.FeeType.Min
FeeType.NORMAL -> AnalyticsParam.FeeType.Normal
FeeType.PRIORITY -> AnalyticsParam.FeeType.Max
}
}
data class FeeState(
val selectedFeeType: FeeType = FeeType.NORMAL,
val fees: TransactionFee? = null,
val currentFee: Fee? = null,
val feeIsIncluded: Boolean = false,
val feeIsApproximate: Boolean = false,
val mainLayoutIsVisible: Boolean = false,
val controlsLayoutIsVisible: Boolean = false,
val feeChipGroupIsVisible: Boolean = true,
val includeFeeSwitcherIsEnabled: Boolean = true,
val progressState: ProgressState = ProgressState.Done,
) : SendScreenState {
override val stateId: StateId = StateId.FEE
fun isReady(): Boolean = currentFee != null
fun getCurrentFeeValue(): BigDecimal = currentFee?.amount?.value ?: BigDecimal.ZERO
}

View file

@ -1,69 +0,0 @@
package com.tangem.tap.features.send.redux.states
// Shows only one type of the layout
enum class ReceiptLayoutType {
UNKNOWN,
FIAT,
CRYPTO,
TOKEN_FIAT,
TOKEN_CRYPTO,
FEE_IN_CUSTOM_TOKEN,
FEE_IN_CUSTOM_TOKEN_FIAT,
SAME_CURRENCY,
SAME_CURRENCY_FIAT,
}
data class ReceiptState(
val visibleTypeOfReceipt: ReceiptLayoutType? = null,
val fiat: ReceiptFiat? = null,
val crypto: ReceiptCrypto? = null,
val tokenFiat: ReceiptTokenFiat? = null,
val tokenCrypto: ReceiptTokenCrypto? = null,
val customTokenFiat: ReceiptTokenFiat? = null,
val customTokenCrypto: ReceiptTokenCrypto? = null,
val sameCurrencyFiat: ReceiptFiat? = null,
val sameCurrencyCrypto: ReceiptCrypto? = null,
val mainCurrency: MainCurrency? = null,
) : SendScreenState {
override val stateId: StateId = StateId.RECEIPT
}
data class ReceiptSymbols(
val fiat: String,
val crypto: String,
val token: String? = null,
val fee: String? = null,
)
data class ReceiptFiat(
val amountFiat: String,
val feeFiat: String,
val totalFiat: String,
val willSentCrypto: String,
val symbols: ReceiptSymbols,
)
data class ReceiptCrypto(
val amountCrypto: String,
val feeCrypto: String,
val totalCrypto: String,
val feeFiat: String,
val willSentFiat: String,
val symbols: ReceiptSymbols,
)
data class ReceiptTokenCrypto(
val amountToken: String,
val feeCoin: String,
val totalFiat: String,
val symbols: ReceiptSymbols,
)
data class ReceiptTokenFiat(
val amountFiat: String,
val feeFiat: String,
val totalFiat: String,
val willSentToken: String,
val willSentFeeCoin: String,
val symbols: ReceiptSymbols,
)

View file

@ -1,196 +0,0 @@
package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface IdStateHolder {
val stateId: StateId
}
enum class StateId {
SEND_SCREEN, ADDRESS_PAY_ID, TRANSACTION_EXTRAS, AMOUNT, FEE, RECEIPT
}
interface SendScreenState : StateType, IdStateHolder
data class SendState(
val walletManager: WalletManager? = null,
val currency: CryptoCurrency? = null,
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val customFeeConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressState: AddressState = AddressState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED),
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null,
val isSuccessSend: Boolean = false,
val canIncludeFee: Boolean = false,
) : SendScreenState {
override val stateId: StateId = StateId.SEND_SCREEN
fun getDecimals(type: MainCurrencyType): Int = when (type) {
MainCurrencyType.FIAT -> 2
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
}
private fun convertFiatToCoin(value: BigDecimal): BigDecimal {
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
private fun convertFiatToToken(value: BigDecimal): BigDecimal {
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}
private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.coinIsConvertible()) return value
val converter = coinConverter!!
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.tokenIsConvertible()) return value
val converter = tokenConverter!!
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertFiatToExtractCrypto(fiatValue: BigDecimal): BigDecimal = when (amountState.typeOfAmount) {
AmountType.Coin -> convertFiatToCoin(fiatValue)
is AmountType.Token -> convertFiatToToken(fiatValue)
is AmountType.FeeResource,
AmountType.Reserve,
-> fiatValue
}
fun convertExtractCryptoToFiat(cryptoValue: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
return when (amountState.typeOfAmount) {
AmountType.Coin -> convertCoinToFiat(cryptoValue, scaleWithPrecision)
is AmountType.Token -> convertTokenToFiat(cryptoValue, scaleWithPrecision)
is AmountType.FeeResource,
AmountType.Reserve,
-> cryptoValue
}
}
fun getButtonState(): ButtonState = if (isReadyToSend()) ButtonState.ENABLED else ButtonState.DISABLED
fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal {
val needToExtractFee = amountState.canIncludeFee() && feeState.feeIsIncluded
return if (needToExtractFee) value.minus(feeState.getCurrentFeeValue()) else value
}
fun coinIsConvertible(): Boolean = coinConverter != null
fun tokenIsConvertible(): Boolean = tokenConverter != null
fun feeIsConvertible(): Boolean = customFeeConverter != null
fun mainCurrencyCanBeSwitched(): Boolean {
return when (amountState.typeOfAmount) {
AmountType.Coin -> coinIsConvertible()
is AmountType.Token -> tokenIsConvertible()
is AmountType.FeeResource,
AmountType.Reserve,
-> false
}
}
companion object {
private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady()
private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
}
}
enum class ButtonState {
ENABLED, DISABLED, PROGRESS
}
data class AmountState(
val amountToExtract: Amount? = null,
val typeOfAmount: AmountType = AmountType.Coin,
val viewAmountValue: InputViewValue = InputViewValue(BigDecimal.ZERO.toPlainString()),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, AppCurrency.Default.code),
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
val hideBalance: Boolean = false,
val cursorAtTheSamePosition: Boolean = true,
val maxLengthOfAmount: Int = 2,
val decimalSeparator: String = ".",
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
fun canIncludeFee(): Boolean = store.state.sendState.canIncludeFee
fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency {
return if (!canSwitched) {
MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE", false)
} else {
when (type) {
MainCurrencyType.FIAT -> MainCurrency(type, store.state.globalState.appCurrency.code)
MainCurrencyType.CRYPTO -> MainCurrency(type, amountToExtract?.currencySymbol ?: "NONE")
}
}
}
fun toBigDecimalSeparator(value: String): String {
return value.replace(",", ".")
}
fun restoreDecimalSeparator(value: String): String {
return DecimalDigitsInputFilter.setDecimalSeparator(value, decimalSeparator)
}
}
data class InputViewValue(val value: String, val isFromUserInput: Boolean = false)
enum class MainCurrencyType {
FIAT, CRYPTO
}
data class MainCurrency(
val type: MainCurrencyType,
val currencySymbol: String,
val isEnabled: Boolean = true,
)
data class ExternalTransactionData(
val amount: String,
val destinationAddress: String,
val transactionId: String,
val canAmountBeModified: Boolean = false,
val canDestinationAddressBeModified: Boolean = false,
)

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.send.ui
import android.R
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextCustomPaste : TextInputEditText {
private var onSystemPasteButtonClickListener: (() -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun setOnSystemPasteButtonClickListener(callback: () -> Unit) {
onSystemPasteButtonClickListener = callback
}
override fun onTextContextMenuItem(id: Int): Boolean {
val isConsumed = super.onTextContextMenuItem(id)
if (id == R.id.paste) onSystemPasteButtonClickListener?.invoke()
return isConsumed
}
}

View file

@ -1,407 +0,0 @@
@file:Suppress("MagicNumber")
package com.tangem.tap.features.send.ui
import android.content.Context
import android.os.Bundle
import android.text.method.DigitsKeyListener
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import java.text.DecimalFormatSymbols
import javax.inject.Inject
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
@OptIn(FlowPreview::class)
@AndroidEntryPoint
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val viewModel by viewModels<SendViewModel>()
lateinit var sendBtn: ViewStateWidget
private lateinit var etAmountToSend: TextInputEditText
private lateinit var warningsAdapter: WarningMessagesAdapter
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
@Inject
lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase
@Inject
lateinit var parseQrCodeUseCase: ParseQrCodeUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
sendSubscriber.initViewModel(viewModel)
Analytics.send(Token.Send.ScreenOpened())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
subscribeToTransactionExtrasFields()
subscribeToAddressField()
subscribeToAmountField()
}
}
addBackPressHandler(this)
etAmountToSend = view.findViewById(R.id.etAmountToSend)
initSendButtonStates()
setupAddressLayout()
setupAmountLayout()
setupFeeLayout()
setupWarningMessages()
store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided)
}
private fun initSendButtonStates() = with(binding) {
btnSend.setOnClickListener {
store.dispatch(
SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header)),
),
)
}
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressLayout() = with(binding.lSendAddress) {
store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddress.apply {
setOnSystemPasteButtonClickListener {
store.dispatch(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
),
)
}
setOnFocusChangeListener { _, hasFocus ->
store.dispatch(TruncateOrRestore(!hasFocus))
}
}
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
store.dispatch(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
),
)
store.dispatch(TruncateOrRestore(!etAddress.isFocused))
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
store.dispatchNavigationAction {
push(AppRoute.QrScanning(source = SourceType.SEND))
}
}
}
private fun CoroutineScope.subscribeToAddressField() = with(binding.lSendAddress) {
etAddress.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(this@subscribeToAddressField)
}
private fun CoroutineScope.subscribeToAmountField() {
etAmountToSend.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(this)
}
private fun CoroutineScope.subscribeToTransactionExtrasFields() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xlmMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XlmMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etDestinationTag.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xrpDestinationTag?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etBinanceMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.binanceMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etTonMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.tonMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etCosmosMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.cosmosMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etHederaMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.hederaMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.HederaMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
etAlgorandMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.algorandMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.AlgorandMemo.HandleUserInput(it)) }
.launchIn(this@subscribeToTransactionExtrasFields)
}
private fun setupAmountLayout() {
store.dispatch(SetMainCurrency(restoreMainCurrency()))
store.dispatch(ReceiptAction.RefreshReceipt)
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator))
binding.lSendAmount.tvAmountCurrency.setOnClickListener {
Analytics.send(Token.Send.ButtonSwapCurrency())
store.dispatch(ToggleMainCurrency)
store.dispatch(ReceiptAction.RefreshReceipt)
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
}
val maxAmountSnackbar = MaxAmountSnackbar.make(etAmountToSend) {
etAmountToSend.clearFocus()
etAmountToSend.postDelayed(200) { etAmountToSend.hideSoftKeyboard() }
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.keyListener = DigitsKeyListener.getInstance("0123456789,.")
etAmountToSend.setOnFocusChangeListener { _, 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.setOnImeActionListener(EditorInfo.IME_ACTION_DONE) {
it.hideSoftKeyboard()
it.clearFocus()
}
}
private fun setupFeeLayout() = with(binding.clNetworkFee) {
flExpandCollapse.flExpandCollapse.setOnClickListener {
store.dispatch(ToggleControlsVisibility)
}
chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL))
chipGroup.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId)))
store.dispatch(CheckAmountToSend)
}
swIncludeFee.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(ChangeIncludeFee(isChecked))
store.dispatch(CheckAmountToSend)
}
}
private fun setupWarningMessages() = with(binding) {
warningsAdapter = WarningMessagesAdapter()
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpaceItemDecoration.all(16f))
rvWarningMessages.adapter = warningsAdapter
store.dispatch(SendAction.Warnings.Update)
}
override fun subscribeToStore() {
store.subscribe(sendSubscriber) { appState ->
appState.skipRepeats { oldState, newState ->
oldState.sendState == newState.sendState
}.select { it.sendState }
}
storeSubscribersList.add(sendSubscriber)
}
private fun restoreMainCurrency(): MainCurrencyType {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
val mainCurrency = sp.getString("mainCurrency", AppCurrency.Default.code)
return MainCurrencyType.values()
.firstOrNull { it.name.equals(mainCurrency!!, ignoreCase = true) }
?: MainCurrencyType.CRYPTO
}
fun saveMainCurrency(type: MainCurrencyType) {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
sp.edit().putString("mainCurrency", type.name).apply()
}
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatchNavigationAction(AppRouter::pop)
} else {
store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
}
}
override fun onDestroyView() {
keyboardObserver.unregisterListener()
super.onDestroyView()
}
override fun onDestroy() {
store.dispatch(ReleaseSendState)
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
}
fun EditText.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = addTextChangedListener { editable -> trySend(editable?.toString() ?: "") }
awaitClose { removeTextChangedListener(watcher) }
}
object FeeUiHelper {
fun toId(fee: FeeType): Int {
return when (fee) {
FeeType.SINGLE -> View.NO_ID
FeeType.LOW -> R.id.chipLow
FeeType.NORMAL -> R.id.chipNormal
FeeType.PRIORITY -> R.id.chipPriority
}
}
fun toType(id: Int): FeeType {
return when (id) {
R.id.chipLow -> FeeType.LOW
R.id.chipNormal -> FeeType.NORMAL
R.id.chipPriority -> FeeType.PRIORITY
else -> FeeType.NORMAL
}
}
}

View file

@ -1,140 +0,0 @@
package com.tangem.tap.features.send.ui
import androidx.lifecycle.*
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase
import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.di.DelayedWork
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class SendViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
@DelayedWork private val coroutineScope: CoroutineScope,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
init {
listenToQrScanningUseCase(SourceType.SEND)
.getOrElse { emptyFlow() }
.onEach(::onQRCodeScanned)
.launchIn(viewModelScope)
}
private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY]
override fun onCreate(owner: LifecycleOwner) {
getBalanceHidingSettingsUseCase()
.flowWithLifecycle(owner.lifecycle)
.onEach {
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(it.isBalanceHidden))
}
.flowOn(dispatchers.main)
.launchIn(viewModelScope)
}
fun updateCurrencyDelayed() {
if (cryptoCurrency != null) {
coroutineScope.launch {
getSelectedWalletSyncUseCase()
.fold(
ifLeft = { Timber.e(it.toString()) },
ifRight = { wallet ->
// we should update network to find pending tx after 1 sec
updateForPendingTx(wallet, cryptoCurrency.network)
// we should update network for new balance
updateForBalance(wallet, cryptoCurrency.network)
},
)
}
} else {
Timber.w("$TAG: cryptoCurrency is null, legacy flow")
}
}
private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) {
fetchPendingTransactionsUseCase(userWallet.walletId, setOf(network))
}
private suspend fun updateForBalance(userWallet: UserWallet, network: Network) {
updateDelayedCurrencyStatusUseCase(
userWalletId = userWallet.walletId,
network = network,
delayMillis = UPDATE_BALANCE_DELAY_MILLIS,
refresh = true,
)
}
private fun onQRCodeScanned(qrScanResult: String) {
if (cryptoCurrency != null) {
parseQrCodeUseCase(qrScanResult, cryptoCurrency).fold(
ifRight = { parsedCode ->
store.dispatch(
AddressActionUi.PasteAddress(
data = parsedCode.address,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
parsedCode.amount?.let { amount ->
store.dispatch(AmountAction.SetAmount(amount, isUserInput = false))
}
// parsedCode.memo?.let { }
},
ifLeft = {
store.dispatch(
AddressActionUi.PasteAddress(
data = qrScanResult,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
Timber.w(it)
},
)
} else {
store.dispatch(
AddressActionUi.PasteAddress(
data = qrScanResult,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
}
store.dispatch(AddressActionUi.TruncateOrRestore(truncate = true))
}
companion object {
private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
private const val TAG = "SendViewModel"
}
}

View file

@ -1,122 +0,0 @@
package com.tangem.tap.features.send.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutWarningCardActionBinding
import timber.log.Timber
// TODO: Delete with SendFeatureToggles
@Deprecated(message = "Used only in old send screen")
class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(DiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH {
val binding = LayoutWarningCardActionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)
return WarningMessageVH(binding)
}
override fun onBindViewHolder(holder: WarningMessageVH, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<WarningMessage>() {
override fun areContentsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
override fun areItemsTheSame(oldItem: WarningMessage, newItem: WarningMessage) = oldItem == newItem
}
}
class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(warning: WarningMessage) {
setBgColor(warning.priority)
setText(warning)
setupControlButtons(warning)
}
private fun setText(warning: WarningMessage) = with(binding.warningContentContainer) {
fun getString(resId: Int?, default: String, formatArgs: String? = null) =
if (resId == null) default else root.getString(resId, formatArgs)
tvTitle.text = getString(
resId = warning.titleResId,
default = warning.title,
formatArgs = warning.titleFormatArg,
)
tvMessage.text = getString(
resId = warning.messageResId,
default = warning.message,
formatArgs = warning.messageFormatArg,
)
}
private fun setBgColor(priority: WarningMessage.Priority) {
val color = when (priority) {
WarningMessage.Priority.Info -> R.color.warning_info
WarningMessage.Priority.Warning -> R.color.warning_warning
WarningMessage.Priority.Critical -> R.color.warning_critical
}
binding.warningCardAction.setCardBackgroundColor(binding.root.getColor(color))
}
@Suppress("LongMethod")
private fun setupControlButtons(warning: WarningMessage) = when (warning.type) {
WarningMessage.Type.Permanent, WarningMessage.Type.TestCard -> {
binding.groupControlsTemporary.hide()
binding.btnClose.hide()
}
WarningMessage.Type.Temporary -> {
binding.groupControlsTemporary.show()
binding.btnClose.hide()
val buttonAction = View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
val buttonTitle = binding.root.getString(
warning.buttonTextId ?: R.string.how_to_got_it_button,
)
binding.btnGotIt.setOnClickListener(buttonAction)
binding.btnGotIt.text = buttonTitle
}
WarningMessage.Type.AppRating -> {
binding.groupControlsTemporary.hide()
binding.btnClose.show()
binding.btnClose.setOnClickListener {
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed))
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
binding.btnReallyCool.setOnClickListener {
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked))
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()
task.addOnCompleteListener {
if (!it.isSuccessful) {
Timber.e(task.exception)
}
}.addOnFailureListener {
Timber.e(it)
}
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
// TODO: [REDACTED_JIRA]
object ChiaWarningDialog {
fun create(context: Context, dialog: SendAction.Dialog.ChiaWarningDialog): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(
context.getString(
R.string.common_utxo_validate_withdrawal_message_warning,
dialog.blockchainName,
dialog.maxOutputs,
dialog.maxAmount.toPlainString(),
),
)
setPositiveButton(R.string.common_ok) { _, _ ->
dialog.onOk()
}
setOnDismissListener {
store.dispatch(SendAction.Dialog.Hide)
}
}
.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
object KaspaWarningDialog {
fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(
context.getString(
R.string.common_utxo_validate_withdrawal_message_warning,
Blockchain.Kaspa.fullName,
dialog.maxOutputs,
dialog.maxAmount.toPlainString(),
),
)
setPositiveButton(R.string.common_ok) { _, _ ->
dialog.onOk()
}
setOnDismissListener {
store.dispatch(SendAction.Dialog.Hide)
}
}.create()
}
}

View file

@ -1,38 +0,0 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object RequestFeeErrorDialog {
fun create(context: Context, dialog: SendAction.Dialog.RequestFeeError): AlertDialog {
val errorMessage = dialog.error.customMessage
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_fee_error)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(
GlobalAction.SendEmail(
feedbackData = SendTransactionFailedEmail(errorMessage),
scanResponse = dialog.scanResponse,
),
)
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}

View file

@ -1,65 +0,0 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object SendTransactionFailsDialog {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context), dialog.scanResponse)
}
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error), dialog.scanResponse)
}
private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse))
}
setPositiveButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}
private class BlockchainSdkErrorConverter(
private val context: Context,
) : ModuleMessageConverter<BlockchainSdkError, String> {
override fun convert(message: BlockchainSdkError): String {
return when (message) {
is BlockchainSdkError.CreateAccountUnderfunded -> {
val resStringId = when (message.blockchain) {
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.string.no_account_polkadot
else -> R.string.send_error_no_target_account
}
val reserveValueString = message.minReserve.value?.stripZeroPlainString() ?: "0"
val argument = "$reserveValueString ${message.minReserve.currencySymbol}"
context.getString(resStringId, argument)
}
else -> message.customMessage
}
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
class TezosWarningDialog(context: Context) : AlertDialog(context) {
companion object {
fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog {
val reduceAmount = showDialogData.reduceAmount.toPlainString()
return Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount))
setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ ->
showDialogData.sendAllCallback()
}
setPositiveButton(context.getString(R.string.xtz_withdrawal_message_reduce, reduceAmount)) { _, _ ->
showDialogData.reduceCallback()
}
setOnDismissListener {
store.dispatch(SendAction.Dialog.Hide)
}
}.create()
}
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import com.tangem.tap.features.BaseStoreFragment
import org.rekotlin.StateType
import org.rekotlin.StoreSubscriber
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
abstract class FragmentStateSubscriber<S : StateType>(fragment: BaseStoreFragment) : StoreSubscriber<S> {
private val weakFragment: WeakReference<BaseStoreFragment> = WeakReference(fragment)
abstract fun updateWithNewState(fg: BaseStoreFragment, state: S)
override fun newState(state: S) {
val fg = weakFragment.get() ?: return
updateWithNewState(fg, state)
}
}

View file

@ -1,530 +0,0 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import android.app.Dialog
import android.content.Context
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.SendViewModel
import com.tangem.tap.features.send.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.send.ui.dialogs.*
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Suppress("LargeClass")
internal class SendStateSubscriber(
fragment: BaseStoreFragment,
) : FragmentStateSubscriber<SendState>(fragment) {
private var dialog: Dialog? = null
private var sendViewModel: SendViewModel? = null
fun initViewModel(viewModel: SendViewModel) {
sendViewModel = viewModel
}
override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) {
fg.view ?: return
if (fg !is SendFragment) return
if (state.isSuccessSend) {
sendViewModel?.updateCurrencyDelayed()
return
}
val lastChangedStates = state.lastChangedStates.toList()
state.lastChangedStates.clear()
(fg as? SendFragment)?.binding?.mainSendContainer?.beginDelayedTransition()
lastChangedStates.forEach {
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState, state.feeState.progressState)
}
}
}
@Deprecated("Legacy")
@Suppress("ComplexMethod", "LongMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddress) {
fun showView(view: View, info: Any?) {
view.show(info != null)
}
showView(xlmMemoContainer, infoState.xlmMemo)
showView(xrpDestinationTagContainer, infoState.xrpDestinationTag)
showView(binanceMemoContainer, infoState.binanceMemo)
showView(tonMemoContainer, infoState.tonMemoState)
showView(cosmosMemoContainer, infoState.cosmosMemoState)
showView(hederaMemoContainer, infoState.hederaMemoState)
showView(algorandMemoContainer, infoState.algorandMemoState)
infoState.xlmMemo?.let {
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
if (it.error != null) {
if (it.error == TransactionExtraError.INVALID_XLM_MEMO) {
tilXlmMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
}
} else {
tilXlmMemo.error = null
}
}
infoState.xrpDestinationTag?.let {
if (infoState.xrpDestinationTag.error != null) {
if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) {
tilDestinationTag.error =
fg.getText(R.string.send_extras_error_invalid_destination_tag)
}
} else {
tilDestinationTag.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etDestinationTag.setText(it.viewFieldValue.value)
}
}
infoState.binanceMemo?.let {
if (infoState.binanceMemo.error != null) {
if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) {
tilBinanceMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
}
} else {
tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etBinanceMemo.setText(it.viewFieldValue.value)
}
}
infoState.tonMemoState?.let {
if (infoState.tonMemoState.error != null) {
tilTonMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etTonMemo.setText(it.viewFieldValue.value)
}
}
infoState.cosmosMemoState?.let {
if (infoState.cosmosMemoState.error != null) {
tilCosmosMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etCosmosMemo.setText(it.viewFieldValue.value)
}
}
infoState.hederaMemoState?.let {
if (infoState.hederaMemoState.error != null) {
tilHederaMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilHederaMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etHederaMemo.setText(it.viewFieldValue.value)
}
}
infoState.algorandMemoState?.let {
if (infoState.algorandMemoState.error != null) {
tilAlgorandMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilAlgorandMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etAlgorandMemo.setText(it.viewFieldValue.value)
}
}
}
@Suppress("ComplexMethod")
private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) {
when (state.dialog) {
is SendAction.Dialog.TezosWarningDialog -> {
if (dialog == null) {
dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.KaspaWarningDialog -> {
if (dialog == null) {
dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.ChiaWarningDialog -> {
if (dialog == null) {
dialog = ChiaWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails.CardSdkError -> {
if (dialog == null) {
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> {
if (dialog == null) {
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.RequestFeeError -> {
if (dialog == null) {
dialog = RequestFeeErrorDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
else -> {
dialog?.dismiss()
dialog = null
}
}
fg.sendBtn.changeState(state.sendButtonState.progressState)
fg.sendBtn.mainView.isEnabled = state.sendButtonState.enabled
val rv = rvWarningMessages
val adapter = rv.adapter as? WarningMessagesAdapter ?: return
adapter.submitList(state.sendWarningsList)
rv.show(state.sendWarningsList.isNotEmpty())
toolbar.title = fg.getString(
R.string.send_title_currency_format,
state.amountState.mainCurrency.currencySymbol,
)
}
private fun handleAddressState(fg: SendFragment, state: AddressState) {
with(fg.binding.lSendAddress) {
fun parseError(context: Context, error: Error?): String? {
val resId = when (error) {
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address
Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet
else -> null
}
return if (resId == null) null else context.getString(resId, "", "")
}
imvPaste.isEnabled = state.pasteIsEnabled
val et = etAddress
val til = tilAddress
val parsedError = parseError(til.context, state.error)
til.isEnabled = state.inputIsEnabled
imvPaste.show(state.inputIsEnabled)
imvQrCode.show(state.inputIsEnabled)
flPaste.show(state.inputIsEnabled)
flQrCode.show(state.inputIsEnabled)
til.hint = til.getString(R.string.send_destination_hint_address)
til.error = parsedError
til.isErrorEnabled = parsedError != null
til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
}
}
private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) {
if (state.error != null) {
val context = fg.requireContext()
val message = when (state.error) {
is MultiMessageError -> {
val multiError = state.error as MultiMessageError
val messageList = multiError.assembleErrors()
.map { getMessageString(context, it.first, it.second) }
multiError.builder(messageList)
}
else -> context.getString(state.error.messageResource)
}
tilAmountToSend.enableError(true, message)
} else {
tilAmountToSend.enableError(false)
}
val filter = DecimalDigitsInputFilter(
digitsBeforeDecimal = 40,
digitsAfterDecimal = state.maxLengthOfAmount,
decimalSeparator = state.decimalSeparator,
)
etAmountToSend.filters = arrayOf(filter)
val amountToSend = state.viewAmountValue
if (!amountToSend.isFromUserInput) etAmountToSend.update(amountToSend.value)
tvAmountCurrency.update(state.mainCurrency.currencySymbol)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
val balanceText = when (state.mainCurrency.type) {
MainCurrencyType.FIAT -> fg.getString(
R.string.common_balance,
"${state.viewBalanceValue} ${state.mainCurrency.currencySymbol}",
).remove(":")
MainCurrencyType.CRYPTO -> fg.getString(
R.string.common_balance,
"${state.mainCurrency.currencySymbol} ${state.viewBalanceValue}",
)
}
tvBalance.update(balanceText)
tilAmountToSend.isEnabled = state.inputIsEnabled
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary
tvAmountCurrency.setTextColor(fg.getColor(textColor))
}
@Suppress("MagicNumber")
private fun handleFeeState(fg: SendFragment, state: FeeState) {
with(fg.binding.clNetworkFee) {
fg.view?.findViewById<ViewGroup>(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible)
flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f
llFeeControlsContainer.show(state.controlsLayoutIsVisible)
chipGroup.show(state.feeChipGroupIsVisible)
swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled
if (swIncludeFee.isChecked != state.feeIsIncluded) {
swIncludeFee.isChecked = state.feeIsIncluded
}
val chipId = FeeUiHelper.toId(state.selectedFeeType)
if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId)
}
with(fg.binding.clReceiptContainer) {
when (state.progressState) {
ProgressState.Loading -> {
tvReceiptFeeValue.hide()
pbReceiptFee.show()
}
ProgressState.Done -> {
pbReceiptFee.hide()
tvReceiptFeeValue.show()
}
else -> {}
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleReceiptState(fg: SendFragment, state: ReceiptState, feeProgressState: ProgressState) {
with(fg.binding.clReceiptContainer) {
val mainLayout = clReceiptContainer as ViewGroup
val totalLayout = llTotalContainer.llTotal as ViewGroup
val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup
fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings)
fun roughOrEmpty(value: String): String {
return if (value == BigDecimalFormatter.EMPTY_BALANCE_SIGN) value else "$ROUGH_SIGN $value"
}
when (feeProgressState) {
ProgressState.Loading -> {
tvReceiptFeeValue.hide()
pbReceiptFee.show()
}
ProgressState.Done -> {
pbReceiptFee.hide()
tvReceiptFeeValue.show()
}
else -> {}
}
when (state.visibleTypeOfReceipt) {
ReceiptLayoutType.FIAT -> {
val receipt = state.fiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_format,
"${receipt.willSentCrypto} ${receipt.symbols.crypto}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.CRYPTO -> {
val receipt = state.crypto ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}")
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}")
}
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
llTotalContainer.tvWillBeSentValue.hide()
} else {
llTotalContainer.tvWillBeSentValue.show()
llTotalContainer.tvWillBeSentValue.update(
getString(
R.string.send_total_subtitle_fiat_format,
"${receipt.willSentFiat} ${receipt.symbols.fiat}",
"${receipt.feeFiat} ${receipt.symbols.fiat}",
),
)
}
}
ReceiptLayoutType.TOKEN_FIAT -> {
val receipt = state.tokenFiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_asset_format,
"${receipt.symbols.token ?: ""} ${receipt.willSentToken}",
"${receipt.symbols.crypto} ${receipt.willSentFeeCoin}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.TOKEN_CRYPTO -> {
val receipt = state.tokenCrypto ?: return
totalLayout.show(false)
totalTokenLayout.show(true)
tvReceiptAmountValue.update("${receipt.amountToken} ${receipt.symbols.token}")
tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.crypto}")
val willSent = SpannableStringBuilder()
.bold {
append(roughOrEmpty(receipt.totalFiat)).append(" ")
append(receipt.symbols.fiat)
}
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
}
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN -> {
val receipt = state.customTokenCrypto ?: return
totalLayout.show(false)
totalTokenLayout.show(true)
tvReceiptAmountValue.update(
"${receipt.amountToken} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
tvReceiptFeeValue.update("${receipt.feeCoin} ${receipt.symbols.fee}")
val willSent = SpannableStringBuilder()
.bold {
append(roughOrEmpty(receipt.totalFiat)).append(" ")
append(receipt.symbols.fiat)
}
llTotalContainer.tvTotalTokenCryptoValue.update(willSent.toString())
}
ReceiptLayoutType.FEE_IN_CUSTOM_TOKEN_FIAT -> {
val receipt = state.customTokenFiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_asset_format,
"${receipt.symbols.token ?: receipt.symbols.crypto} ${receipt.willSentToken}",
"${receipt.symbols.fee} ${receipt.willSentFeeCoin}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.SAME_CURRENCY -> {
val receipt = state.sameCurrencyCrypto ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update(
"${receipt.amountCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${receipt.totalCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
}
if (receipt.willSentFiat == BigDecimalFormatter.EMPTY_BALANCE_SIGN) {
llTotalContainer.tvWillBeSentValue.hide()
} else {
llTotalContainer.tvWillBeSentValue.show()
llTotalContainer.tvWillBeSentValue.update(
getString(
R.string.send_total_subtitle_fiat_format,
"${receipt.willSentFiat} ${receipt.symbols.fiat}",
"${receipt.feeFiat} ${receipt.symbols.fiat}",
),
)
}
}
ReceiptLayoutType.SAME_CURRENCY_FIAT -> {
val receipt = state.sameCurrencyFiat ?: return
totalLayout.show(true)
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
llTotalContainer.tvTotalValue.post {
llTotalContainer.tvTotalValue.update(
"${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
)
}
val willSent = getString(
R.string.send_total_subtitle_format,
"${receipt.willSentCrypto} ${receipt.symbols.token ?: receipt.symbols.crypto}",
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}
ReceiptLayoutType.UNKNOWN, null -> {}
}
}
}
private companion object {
const val ROUGH_SIGN = ""
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
@ -17,11 +15,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -45,9 +40,6 @@ object TradeCryptoMiddleware {
}
}
private val isSendRedesignedEnabled: Boolean
get() = store.inject(getDependency = DaggerGraphState::sendFeatureToggles).isRedesignedSendEnabled
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -62,20 +54,8 @@ object TradeCryptoMiddleware {
cryptoCurrencyId = action.cryptoCurrencyId,
yield = action.yield,
)
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
} else {
handleSendToken(action = action)
}
}
is TradeCryptoAction.SendCoin -> {
if (isSendRedesignedEnabled) {
handleNewSendCoin(action = action)
} else {
handleSendCoin(action = action)
}
}
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
}
}
@ -177,135 +157,6 @@ object TradeCryptoMiddleware {
}
}
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
val currency = action.tokenCurrency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmount = walletManager.wallet.amounts.values.firstOrNull {
val amountType = it.type
amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
currency = currency,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmount,
tokenRate = action.tokenFiatRate,
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
),
)
val txInfo = action.transactionInfo
if (txInfo != null) {
store.dispatchOnMain(
SendAction.SendSpecificTransaction(
sendAmount = txInfo.amount,
destinationAddress = txInfo.destinationAddress,
transactionId = txInfo.transactionId,
),
)
}
val route = AppRoute.Send(
currency = currency,
userWalletId = action.userWallet.walletId,
)
store.dispatchNavigationAction { push(route) }
}
}
private fun handleSendCoin(action: TradeCryptoAction.SendCoin) {
if (action.transactionInfo?.tag != null) {
// avoid open old send if memo exists
return
}
val cryptoStatus = action.coinStatus
val currency = cryptoStatus.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin }
when (currency) {
is CryptoCurrency.Coin -> {
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol }
if (amountToSend == null) {
val error = TapError.UnsupportedState(stateError = "Amount to send is null")
FirebaseCrashlytics.getInstance()
.recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(),
currency = currency,
coinAmount = amountToSend,
coinRate = cryptoStatus.value.fiatRate,
feeCurrencyRate = action.feeCurrencyStatus?.value?.fiatRate,
feeCurrencyDecimals = action.feeCurrencyStatus?.currency?.decimals ?: 0,
),
)
}
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
}
val txInfo = action.transactionInfo
if (txInfo != null) {
store.dispatchOnMain(
SendAction.SendSpecificTransaction(
sendAmount = txInfo.amount,
destinationAddress = txInfo.destinationAddress,
transactionId = txInfo.transactionId,
),
)
}
val route = AppRoute.Send(
currency = currency,
userWalletId = action.userWallet.walletId,
)
store.dispatchNavigationAction { push(route) }
}
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId,

View file

@ -32,7 +32,6 @@ import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
import com.tangem.features.tester.api.TesterRouter
@ -64,7 +63,6 @@ data class DaggerGraphState(
val balanceHidingRepository: BalanceHidingRepository? = null,
val walletsRepository: WalletsRepository? = null,
val networksRepository: NetworksRepository? = null,
val sendFeatureToggles: SendFeatureToggles? = null,
val sendRouter: SendRouter? = null,
val qrScanningRouter: QrScanningRouter? = null,
val currenciesRepository: CurrenciesRepository? = null,

View file

@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_secondary"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:navigationIconTint="@color/text_primary_1"
app:title="@string/common_send"
app:titleTextColor="@color/text_primary_1" />
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:id="@+id/main_send_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/l_send_address"
layout="@layout/layout_send_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />
<include
android:id="@+id/l_send_amount"
layout="@layout/layout_send_amount"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp" />
<include
android:id="@+id/clNetworkFee"
layout="@layout/layout_send_fee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
tools:visibility="visible" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:minHeight="1dp" />
<include
android:id="@+id/clReceiptContainer"
layout="@layout/layout_send_receipt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="24dp"
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:visibility="gone" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/llBottomButtonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<FrameLayout
android:id="@+id/flSendButtonContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:clipToPadding="false"
android:paddingBottom="33dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSend"
style="@style/TapPrimaryIconButtonLegacy"
android:layout_width="match_parent"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/common_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/background_primary" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,329 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/csRecipientAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/addressContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAddress"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:errorIconDrawable="@null"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
style="@style/SecondaryTextInputLayout"
app:layout_constraintTop_toTopOf="parent">
<com.tangem.tap.features.send.ui.EditTextCustomPaste
android:id="@+id/etAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:inputType="textNoSuggestions"
android:paddingStart="0dp"
android:paddingEnd="98dp"
android:singleLine="true"
android:textSize="16sp"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
tools:text="139mrsJgyWnJ **** y9BV" />
</com.google.android.material.textfield.TextInputLayout>
<FrameLayout
android:id="@+id/flPaste"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:layout_marginEnd="8dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintBottom_toBottomOf="@+id/flQrCode"
app:layout_constraintEnd_toStartOf="@+id/flQrCode"
app:layout_constraintTop_toTopOf="@+id/flQrCode">
<ImageView
android:id="@+id/imvPaste"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="6dp"
app:srcCompat="@drawable/paste_selector" />
</FrameLayout>
<FrameLayout
android:id="@+id/flQrCode"
android:layout_width="@dimen/btn_rounded_size"
android:layout_height="@dimen/btn_rounded_size"
android:background="@drawable/shape_ellipse"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="4dp"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/imvQrCode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:padding="5dp"
app:srcCompat="@drawable/ic_qr_code_scan" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/extrasFieldsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addressContainer">
<LinearLayout
android:id="@+id/xlmMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilXlmMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etXlmMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="middle"
android:inputType="text|textNoSuggestions"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Some memo" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/binanceMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBinanceMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etBinanceMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:inputType="number"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="123" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/xrpDestinationTagContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilDestinationTag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_destination_tag"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etDestinationTag"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:inputType="number"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Some destination tag" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/tonMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilTonMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etTonMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Ton memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/cosmosMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCosmosMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etCosmosMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Ton memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/hederaMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilHederaMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etHederaMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Hedera memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/algorandMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAlgorandMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
style="@style/SecondaryTextInputLayout"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAlgorandMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_secondary"
android:textColor="@color/text_primary_1"
android:ellipsize="end"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="Algorand memo" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>

View file

@ -1,71 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/amountContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tilAddress">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAmountToSend"
style="@style/SecondaryTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:errorIconDrawable="@null"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAmountToSend"
android:layout_width="match_parent"
android:layout_height="82dp"
android:background="@color/background_secondary"
android:fontFamily="sans-serif-light"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:paddingStart="0dp"
android:paddingEnd="96dp"
android:textColor="@color/text_primary_1"
android:textSize="@dimen/text_size_amount_to_send"
tools:text="139" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvAmountCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="28dp"
android:layout_marginEnd="16dp"
android:drawablePadding="10dp"
android:fontFamily="sans-serif-light"
android:textAllCaps="true"
android:textColor="@color/accent"
android:textSize="32sp"
app:drawableEndCompat="@drawable/ic_arrows_up_down"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="USD" />
<TextView
android:id="@+id/tvBalance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:gravity="end"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/text_tertiary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tilAmountToSend" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,93 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clNetworkFee"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tvNetworkFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/common_network_fee_title"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/flExpandCollapse"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/flExpandCollapse"
layout="@layout/btn_expand_collapse"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llFeeControlsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/flExpandCollapse">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false">
<com.google.android.material.chip.ChipGroup
android:id="@+id/chipGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:translationY="-8dp"
app:selectionRequired="true"
app:singleLine="true"
app:singleSelection="true">
<com.google.android.material.chip.Chip
android:id="@+id/chipLow"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_low" />
<com.google.android.material.chip.Chip
android:id="@+id/chipNormal"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_normal"
tools:text="Нормальный" />
<com.google.android.material.chip.Chip
android:id="@+id/chipPriority"
style="@style/TapChip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_fee_picker_priority"
tools:text="Приоритетный" />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/swIncludeFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:text="@string/send_fee_include_description"
android:textColor="@color/text_primary_1"
android:textSize="13sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,87 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/clReceiptContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/tvReceiptAmount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_amount_label"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvReceiptAmountValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="75.00 usd" />
<TextView
android:id="@+id/tvReceiptFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/common_fee_label"
android:textColor="@color/text_tertiary"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvReceiptAmount" />
<ProgressBar
android:id="@+id/pbReceiptFee"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:elevation="18dp"
android:indeterminate="true"
android:indeterminateTint="@color/accent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/tvReceiptFee"
app:layout_constraintEnd_toEndOf="@+id/tvReceiptFeeValue"
app:layout_constraintTop_toTopOf="@+id/tvReceiptFee" />
<TextView
android:id="@+id/tvReceiptFeeValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAllCaps="true"
android:textColor="@color/text_tertiary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tvReceiptFee"
tools:text="0.03 usd" />
<View
android:id="@+id/delimiter"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="10dp"
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@+id/tvReceiptFee"
tools:layout_editor_absoluteX="0dp" />
<include
android:id="@+id/llTotalContainer"
layout="@layout/layout_receipt_total"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/delimiter" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,5 +0,0 @@
<?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

@ -1,22 +0,0 @@
<?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_max_amount_label"
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

@ -3,10 +3,6 @@
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "undefined"
},
{
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
"version": "5.10.0"
},
{
"name": "LOCAL_USER_LOGS_ENABLED",
"version": "5.13.0"

View file

@ -1,13 +0,0 @@
package com.tangem.features.send.api.featuretoggles
/**
* Send feature toggles
*/
interface SendFeatureToggles {
/** Availability of redesigned send screen */
val isRedesignedSendEnabled: Boolean
/** Updates remote toggle */
suspend fun fetchNewSendEnabled()
}

View file

@ -1,34 +0,0 @@
package com.tangem.features.send.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.features.send.impl.featuretoggles.DefaultSendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* DI module provides implementation of [SendFeatureToggles]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object SendFeatureTogglesModule {
@Provides
@Singleton
fun provideSendFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): SendFeatureToggles {
return DefaultSendFeatureToggles(
featureTogglesManager = featureTogglesManager,
tangemTechApi = tangemTechApi,
dispatchers = dispatchers,
)
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.features.send.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
/**
* Default implementation of Send feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
* @property tangemTechApi api to get remote feature toggle for send
* @property dispatchers coroutine dispatchers
*/
internal class DefaultSendFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : SendFeatureToggles {
private val remoteSendEnabled: MutableStateFlow<Boolean> = MutableStateFlow(true)
override val isRedesignedSendEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_SEND_SCREEN_ENABLED") &&
remoteSendEnabled.value
override suspend fun fetchNewSendEnabled() {
runCatching(dispatchers.io) {
tangemTechApi.getFeatures().getOrThrow()
}.onSuccess { response ->
remoteSendEnabled.update { response.isNewSendEnabled }
}.onFailure {
Timber.e(it.localizedMessage, "Unable to fetch new send toggle")
}
}
}