Updated on 2026-08-14
This commit is contained in:
commit
5204b9982b
547 changed files with 14659 additions and 9373 deletions
|
|
@ -67,6 +67,7 @@ dependencies {
|
|||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.walletConnect)
|
||||
implementation(projects.domain.markets)
|
||||
implementation(projects.domain.manageTokens)
|
||||
|
||||
implementation(projects.common)
|
||||
implementation(projects.common.routing)
|
||||
|
|
@ -103,6 +104,7 @@ dependencies {
|
|||
implementation(projects.data.staking)
|
||||
implementation(projects.data.walletConnect)
|
||||
implementation(projects.data.markets)
|
||||
implementation(projects.data.manageTokens)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
|
|
@ -688,6 +688,26 @@
|
|||
"networkId": "base/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "blast-ethereum",
|
||||
"name": "Blast",
|
||||
"symbol": "ETH",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "blast/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cyberconnect",
|
||||
"name": "Cyber",
|
||||
"symbol": "ETH",
|
||||
"networks": [
|
||||
{
|
||||
"networkId": "cyber/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ 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.log.TangemAppLoggerInitializer
|
||||
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 +82,6 @@ interface ApplicationEntryPoint {
|
|||
|
||||
fun getWalletsRepository(): WalletsRepository
|
||||
|
||||
fun getSendFeatureToggles(): SendFeatureToggles
|
||||
|
||||
fun getOneTimeEventFilter(): OneTimeEventFilter
|
||||
|
||||
fun getGeneralUserWalletsListManager(): UserWalletsListManager
|
||||
|
|
@ -119,4 +117,6 @@ interface ApplicationEntryPoint {
|
|||
fun getAppRouter(): AppRouter
|
||||
|
||||
fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles
|
||||
|
||||
fun getTangemAppLogger(): TangemAppLoggerInitializer
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ internal class LockUserWalletsTimer(
|
|||
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
|
||||
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
|
||||
|
||||
Timber.d(
|
||||
Timber.i(
|
||||
"""
|
||||
Owner resumed
|
||||
|- Was stopped: $wasApplicationStopped
|
||||
|
|
@ -55,7 +55,7 @@ internal class LockUserWalletsTimer(
|
|||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
Timber.d("Owner stopped")
|
||||
Timber.i("Owner stopped")
|
||||
|
||||
owner.lifecycleScope.launch {
|
||||
settingsRepository.setWasApplicationStopped(value = true)
|
||||
|
|
@ -63,13 +63,13 @@ internal class LockUserWalletsTimer(
|
|||
}
|
||||
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
Timber.d("Owner destroyed")
|
||||
Timber.i("Owner destroyed")
|
||||
stop()
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
if (delayJob == null) return
|
||||
Timber.d(
|
||||
Timber.i(
|
||||
"""
|
||||
Timer restart
|
||||
|- Duration millis: ${duration.inWholeMilliseconds}
|
||||
|
|
@ -80,7 +80,7 @@ internal class LockUserWalletsTimer(
|
|||
|
||||
private fun start(log: Boolean = true) {
|
||||
if (log) {
|
||||
Timber.d(
|
||||
Timber.i(
|
||||
"""
|
||||
Timer start
|
||||
|- Duration millis: ${duration.inWholeMilliseconds}
|
||||
|
|
@ -92,7 +92,7 @@ internal class LockUserWalletsTimer(
|
|||
|
||||
private fun stop(log: Boolean = true) {
|
||||
if (log) {
|
||||
Timber.d(
|
||||
Timber.i(
|
||||
"""
|
||||
Timer stop
|
||||
|- Was started: ${delayJob?.isActive ?: false}
|
||||
|
|
@ -114,7 +114,7 @@ internal class LockUserWalletsTimer(
|
|||
val currentTime = System.currentTimeMillis()
|
||||
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
|
||||
|
||||
Timber.d(
|
||||
Timber.i(
|
||||
"""
|
||||
Finished
|
||||
|- App is stopped: $wasApplicationStopped
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import android.content.pm.PackageManager
|
|||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.TangemSdkLogger
|
||||
|
|
@ -47,7 +45,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
|
||||
|
|
@ -55,12 +52,11 @@ import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandle
|
|||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.LegacyFeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.log.TimberFormatStrategy
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.tasks.product.DerivationsFinder
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
|
|
@ -71,7 +67,6 @@ import com.tangem.wallet.BuildConfig
|
|||
import dagger.hilt.EntryPoints
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
|
||||
|
||||
lateinit var store: Store<AppState>
|
||||
|
|
@ -140,9 +135,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()
|
||||
|
||||
|
|
@ -196,6 +188,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles
|
||||
get() = entryPoint.getPushNotificationsFeatureToggles()
|
||||
|
||||
private val tangemAppLoggerInitializer: TangemAppLoggerInitializer
|
||||
get() = entryPoint.getTangemAppLogger()
|
||||
// endregion
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -207,16 +202,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
fun init() {
|
||||
store = createReduxStore()
|
||||
|
||||
if (BuildConfig.LOG_ENABLED) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
Timber.plant(
|
||||
object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
Logger.log(priority, tag, message, t)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
tangemAppLoggerInitializer.initialize()
|
||||
|
||||
foregroundActivityObserver = ForegroundActivityObserver()
|
||||
activityResultCaller = foregroundActivityObserver
|
||||
|
|
@ -232,8 +218,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
}
|
||||
|
||||
initWarningMessagesManager()
|
||||
|
||||
loadNativeLibraries()
|
||||
|
||||
if (LogConfig.network.blockchainSdkNetwork) {
|
||||
|
|
@ -271,7 +255,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
appThemeModeRepository = appThemeModeRepository,
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
walletsRepository = walletsRepository,
|
||||
sendFeatureToggles = sendFeatureToggles,
|
||||
generalUserWalletsListManager = generalUserWalletsListManager,
|
||||
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
|
||||
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
|
||||
|
|
@ -382,8 +365,4 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
|
||||
}
|
||||
|
||||
private fun initWarningMessagesManager() {
|
||||
store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager()))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -12,12 +12,12 @@ fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider
|
|||
val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name
|
||||
|
||||
if (name == currentFragmentName) {
|
||||
Timber.d("Fragment $name is already at the top of the stack")
|
||||
Timber.i("Fragment $name is already at the top of the stack")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Showing $name route")
|
||||
Timber.i("Showing $name route")
|
||||
|
||||
val isPoppedBack = popBackStackImmediate(name, 0)
|
||||
|
||||
|
|
@ -30,9 +30,9 @@ fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider
|
|||
fragment.showFragment(fragmentManager = this, name)
|
||||
}
|
||||
|
||||
Timber.d("Route $name is shown")
|
||||
Timber.i("Route $name is shown")
|
||||
} else {
|
||||
Timber.d("Route $name is found in backstack and shown")
|
||||
Timber.i("Route $name is found in backstack and shown")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
import android.webkit.WebView
|
||||
|
||||
fun WebView.stop() {
|
||||
stopLoading()
|
||||
pauseTimers()
|
||||
}
|
||||
|
|
@ -31,12 +31,6 @@ class FeedbackDataBuilder(
|
|||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
|
||||
// enable later
|
||||
// if (walletInfo.blockchain == Blockchain.Bitcoin) {
|
||||
// builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey)
|
||||
// }
|
||||
|
||||
builder.appendKeyValue("Outputs count", walletInfo.outputsCount)
|
||||
|
||||
if (walletInfo.tokens.isNotEmpty()) {
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
package com.tangem.tap.common.haptic
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
|
||||
class DefaultHapticManager(private val vibrator: Vibrator) : HapticManager {
|
||||
|
||||
override fun vibrateShort() {
|
||||
vibrate(VIBRATION_SHORT_DURATION)
|
||||
}
|
||||
|
||||
override fun vibrateMeduim() {
|
||||
vibrate(VIBRATION_MEDIUM_LOW_DURATION)
|
||||
}
|
||||
|
||||
override fun vibrateLong() {
|
||||
vibrate(VIBRATION_MEDIUM_DURATION)
|
||||
}
|
||||
|
||||
private fun vibrate(durationMs: Long) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
vibrator.vibrate(durationMs)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIBRATION_SHORT_DURATION = 50L
|
||||
const val VIBRATION_MEDIUM_LOW_DURATION = 75L
|
||||
const val VIBRATION_MEDIUM_DURATION = 100L
|
||||
const val VIBRATION_LONG_DURATION = 200L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.tap.common.haptic
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import androidx.annotation.ChecksSdkIntAtLeast
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
|
||||
internal class DefaultVibratorHapticManager(
|
||||
private val vibrator: Vibrator,
|
||||
) : VibratorHapticManager {
|
||||
|
||||
private val isHapticEnabled = deviceSupportsVibrationEffects()
|
||||
|
||||
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q)
|
||||
private fun deviceSupportsVibrationEffects(): Boolean = when {
|
||||
!vibrator.hasVibrator() -> false
|
||||
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
|
||||
vibrator.areAllEffectsSupported(
|
||||
TangemHapticEffect.OneTime.DoubleClick.code,
|
||||
TangemHapticEffect.OneTime.HeavyClick.code,
|
||||
TangemHapticEffect.OneTime.Tick.code,
|
||||
TangemHapticEffect.OneTime.Click.code,
|
||||
) == Vibrator.VIBRATION_EFFECT_SUPPORT_YES
|
||||
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun performOneTime(effect: TangemHapticEffect.OneTime) {
|
||||
if (!isHapticEnabled) return
|
||||
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(effect.code))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import android.util.Log
|
||||
import com.orhanobut.logger.AndroidLogAdapter
|
||||
import com.orhanobut.logger.Logger
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Tangem app logger
|
||||
*
|
||||
* @property settingsRepository repository for saving logs
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TangemAppLoggerInitializer(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
) {
|
||||
|
||||
/** Initialize */
|
||||
fun initialize() {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
|
||||
}
|
||||
|
||||
Timber.plant(tree = createTimberTree())
|
||||
}
|
||||
|
||||
private fun createTimberTree(): Timber.Tree {
|
||||
return object : Timber.DebugTree() {
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
if (IS_LOG_ENABLED) {
|
||||
Logger.log(priority, tag, message, t)
|
||||
}
|
||||
|
||||
if (PERMITTED_PRIORITY.contains(priority)) {
|
||||
settingsRepository.saveLogMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val IS_LOG_ENABLED: Boolean = BuildConfig.LOG_ENABLED
|
||||
val PERMITTED_PRIORITY = listOf(Log.ERROR, Log.INFO)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,6 @@ import com.tangem.Log
|
|||
import com.tangem.LogFormat
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* CardSDK logger implementation
|
||||
|
|
@ -16,7 +11,6 @@ import kotlinx.coroutines.sync.withLock
|
|||
* @property levels logging levels
|
||||
* @property messageFormatter message formatter
|
||||
* @property settingsRepository settings repository
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -24,19 +18,11 @@ internal class TangemCardSDKLogger(
|
|||
private val levels: List<Log.Level>,
|
||||
private val messageFormatter: LogFormat,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemSdkLogger {
|
||||
|
||||
private val scope = CoroutineScope(dispatchers.main)
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun log(message: () -> String, level: Log.Level) {
|
||||
if (!levels.contains(level)) return
|
||||
|
||||
scope.launch(dispatchers.main) {
|
||||
mutex.withLock {
|
||||
settingsRepository.updateAppLogs(message = messageFormatter.format(message, level))
|
||||
}
|
||||
}
|
||||
settingsRepository.saveLogMessage(message = messageFormatter.format(message, level))
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
val logMiddleware: Middleware<AppState> = { dispatch, appState ->
|
||||
val logMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ nextDispatch ->
|
||||
{ action ->
|
||||
if (LogConfig.storeAction) {
|
||||
Timber.d("Dispatch action: $action")
|
||||
// printOnboardingWalletState()
|
||||
}
|
||||
Timber.i("Dispatch action: $action")
|
||||
nextDispatch(action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
|
|
@ -13,8 +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
|
||||
|
||||
sealed class GlobalAction : Action {
|
||||
|
|
@ -72,10 +69,7 @@ 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()
|
||||
|
||||
data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
|
||||
|
|
@ -93,6 +87,4 @@ sealed class GlobalAction : Action {
|
|||
object FetchUserCountry : GlobalAction() {
|
||||
data class Success(val countryCode: String) : GlobalAction()
|
||||
}
|
||||
|
||||
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
is GlobalAction.SetConfigManager -> {
|
||||
globalState.copy(configManager = action.configManager)
|
||||
}
|
||||
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
val card = globalState.scanResponse?.card ?: return globalState
|
||||
val wallet = card.wallets
|
||||
|
|
@ -92,9 +91,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
|||
userCountryCode = action.countryCode,
|
||||
)
|
||||
}
|
||||
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
|
||||
appThemeMode = action.appThemeMode,
|
||||
)
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.feedback.LegacyFeedbackManager
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.StateType
|
||||
|
|
@ -19,7 +18,6 @@ data class GlobalState(
|
|||
val cardVerifiedOnline: Boolean = false,
|
||||
val tapWalletManager: TapWalletManager = TapWalletManager(),
|
||||
val configManager: ConfigManager? = null,
|
||||
val warningManager: WarningMessagesManager? = null,
|
||||
val feedbackManager: LegacyFeedbackManager? = null,
|
||||
val appCurrency: AppCurrency = AppCurrency.Default,
|
||||
val scanCardFailsCounter: Int = 0,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.domain.feedback.models.BlockchainErrorInfo
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.utils.convertToSdkAmount
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
|
|
@ -42,7 +42,7 @@ internal object LegacyMiddleware {
|
|||
is LegacyAction.SendEmailTransactionFailed -> {
|
||||
if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
|
||||
|
||||
val amount = action.amount?.convertToAmount(action.cryptoCurrency)
|
||||
val amount = action.amount?.convertToSdkAmount(action.cryptoCurrency)
|
||||
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
|
||||
error = BlockchainErrorInfo(
|
||||
errorMessage = action.errorMessage,
|
||||
|
|
@ -55,7 +55,7 @@ internal object LegacyMiddleware {
|
|||
""
|
||||
},
|
||||
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
|
||||
fee = action.fee?.convertToAmount(action.cryptoCurrency)
|
||||
fee = action.fee?.convertToSdkAmount(action.cryptoCurrency)
|
||||
?.value?.stripZeroPlainString() ?: "unknown",
|
||||
),
|
||||
)
|
||||
|
|
@ -73,8 +73,8 @@ internal object LegacyMiddleware {
|
|||
)?.let { walletManager ->
|
||||
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
|
||||
walletManager = walletManager,
|
||||
amountToSend = action.amount?.convertToAmount(action.cryptoCurrency),
|
||||
feeAmount = action.fee?.convertToAmount(action.cryptoCurrency),
|
||||
amountToSend = action.amount?.convertToSdkAmount(action.cryptoCurrency),
|
||||
feeAmount = action.fee?.convertToSdkAmount(action.cryptoCurrency),
|
||||
destinationAddress = action.destinationAddress,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,33 +2,19 @@ package com.tangem.tap.data
|
|||
|
||||
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* BlockchainSDK logger implementation
|
||||
*
|
||||
* @property settingsRepository settings repository
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class TangemBlockchainSDKLogger(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : BlockchainSDKLogger {
|
||||
|
||||
private val scope = CoroutineScope(dispatchers.main)
|
||||
private val mutex = Mutex()
|
||||
|
||||
override fun log(level: BlockchainSDKLogger.Level, message: String) {
|
||||
scope.launch(dispatchers.main) {
|
||||
mutex.withLock {
|
||||
settingsRepository.updateAppLogs(message)
|
||||
}
|
||||
}
|
||||
settingsRepository.saveLogMessage(message)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@ import android.content.Context
|
|||
import android.os.Build
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import com.tangem.tap.common.haptic.DefaultHapticManager
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.MockHapticManager
|
||||
import com.tangem.tap.common.haptic.DefaultVibratorHapticManager
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -20,7 +20,7 @@ class HapticModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHapticManager(@ApplicationContext context: Context): HapticManager {
|
||||
fun provideHapticManager(@ApplicationContext context: Context): VibratorHapticManager {
|
||||
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
|
|
@ -29,9 +29,12 @@ class HapticModule {
|
|||
}
|
||||
|
||||
return if (vibrator.hasVibrator()) {
|
||||
DefaultHapticManager(vibrator = vibrator)
|
||||
DefaultVibratorHapticManager(vibrator = vibrator)
|
||||
} else {
|
||||
MockHapticManager
|
||||
// mock
|
||||
object : VibratorHapticManager {
|
||||
override fun performOneTime(effect: TangemHapticEffect.OneTime) = Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.di
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.haptic.HapticManager
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
import dagger.Module
|
||||
|
|
@ -17,9 +17,12 @@ internal object UiDependenciesModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies {
|
||||
fun provideUiDependencies(
|
||||
vibratorHapticManager: VibratorHapticManager,
|
||||
appThemeModeHolder: AppThemeModeHolder,
|
||||
): UiDependencies {
|
||||
return object : UiDependencies {
|
||||
override val hapticManager = hapticManager
|
||||
override val vibratorHapticManager = vibratorHapticManager
|
||||
override val appThemeModeHolder = appThemeModeHolder
|
||||
override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState()
|
||||
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.tap.di.data
|
||||
|
||||
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.tap.data.TangemBlockchainSDKLogger
|
||||
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
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object BlockchainSDKLoggerModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockchainSDKLogger(
|
||||
settingsRepository: SettingsRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): BlockchainSDKLogger {
|
||||
return TangemBlockchainSDKLogger(settingsRepository, dispatchers)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.di.data
|
|||
import com.tangem.Log
|
||||
import com.tangem.LogFormat
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.tap.common.log.TangemAppLoggerInitializer
|
||||
import com.tangem.tap.common.log.TangemCardSDKLogger
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.tap.data.TangemBlockchainSDKLogger
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -14,14 +16,17 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CardSDKLoggerModule {
|
||||
internal object TangemLoggingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCardSDKLogger(
|
||||
settingsRepository: SettingsRepository,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): TangemSdkLogger {
|
||||
fun provideAppLoggerInitializer(settingsRepository: SettingsRepository): TangemAppLoggerInitializer {
|
||||
return TangemAppLoggerInitializer(settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCardSDKLogger(settingsRepository: SettingsRepository): TangemSdkLogger {
|
||||
val logLevels = listOf(
|
||||
Log.Level.ApduCommand,
|
||||
Log.Level.Apdu,
|
||||
|
|
@ -40,7 +45,12 @@ internal object CardSDKLoggerModule {
|
|||
levels = logLevels,
|
||||
messageFormatter = LogFormat.StairsFormatter(),
|
||||
settingsRepository = settingsRepository,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBlockchainSDKLogger(settingsRepository: SettingsRepository): BlockchainSDKLogger {
|
||||
return TangemBlockchainSDKLogger(settingsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
|
||||
|
|
@ -69,8 +70,9 @@ internal object CardDomainModule {
|
|||
@Singleton
|
||||
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
||||
derivationsRepository: DerivationsRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository)
|
||||
return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository, walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.managetokens.GetManagedTokensUseCase
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object ManageTokensDomainModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetManageTokensUseCase(manageTokensRepository: ManageTokensRepository): GetManagedTokensUseCase {
|
||||
return GetManagedTokensUseCase(manageTokensRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
|
||||
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
|
||||
import com.tangem.domain.markets.GetTokenPriceChartUseCase
|
||||
import com.tangem.domain.markets.GetTokenQuotesUseCase
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -19,4 +22,22 @@ object MarketsDomainModule {
|
|||
): GetMarketsTokenListFlowUseCase {
|
||||
return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenPriceChartUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenPriceChartUseCase {
|
||||
return GetTokenPriceChartUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenMarketInfoUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenMarketInfoUseCase {
|
||||
return GetTokenMarketInfoUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
|
||||
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -197,5 +197,11 @@ internal object SettingsDomainModule {
|
|||
): NeverRequestPermissionUseCase {
|
||||
return NeverRequestPermissionUseCase(repository = permissionRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideShouldSaveAccessCodesUseCase(settingsRepository: SettingsRepository): ShouldSaveAccessCodesUseCase {
|
||||
return ShouldSaveAccessCodesUseCase(settingsRepository = settingsRepository)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -156,4 +156,28 @@ internal object StakingDomainModule {
|
|||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsApproveNeededUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): IsApproveNeededUseCase {
|
||||
return IsApproveNeededUseCase(
|
||||
stakingRepository = stakingRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetConstructedStakingTransactionUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): GetConstructedStakingTransactionUseCase {
|
||||
return GetConstructedStakingTransactionUseCase(
|
||||
stakingRepository = stakingRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -413,4 +413,10 @@ internal object TokensDomainModule {
|
|||
quotesRepository = quotesRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase {
|
||||
return CheckHasLinkedTokensUseCase(currenciesRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -100,4 +100,18 @@ internal object TransactionDomainModule {
|
|||
): IsUtxoConsolidationAvailableUseCase {
|
||||
return IsUtxoConsolidationAvailableUseCase(walletManagersFacade)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCreateApproveTransactionUseCase(
|
||||
transactionRepository: TransactionRepository,
|
||||
): CreateApprovalTransactionUseCase {
|
||||
return CreateApprovalTransactionUseCase(transactionRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
|
||||
return GetAllowanceUseCase(transactionRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ import com.tangem.common.doOnSuccess
|
|||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
|
@ -46,12 +44,16 @@ internal class DefaultDerivationsRepository(
|
|||
return
|
||||
}
|
||||
|
||||
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
|
||||
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
|
||||
.doOnSuccess { response ->
|
||||
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
|
||||
.doOnSuccess {
|
||||
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
|
||||
return
|
||||
return response.entries
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
}
|
||||
|
|
@ -60,28 +62,6 @@ internal class DefaultDerivationsRepository(
|
|||
error("This code should never be reached")
|
||||
}
|
||||
|
||||
override suspend fun deriveExtendedPublicKey(
|
||||
userWalletId: UserWalletId,
|
||||
derivation: DerivationPath,
|
||||
): ExtendedPublicKey? {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
val walletCard = userWallet.scanResponse.card.wallets.firstOrNull {
|
||||
UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value
|
||||
.contentEquals(userWallet.walletId.value)
|
||||
} ?: return null
|
||||
|
||||
val result = tangemSdkManager.deriveExtendedPublicKey(
|
||||
cardId = null,
|
||||
walletPublicKey = walletCard.publicKey,
|
||||
derivation = derivation,
|
||||
)
|
||||
|
||||
return when (result) {
|
||||
is CompletionResult.Failure -> throw result.error
|
||||
is CompletionResult.Success -> result.data
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.tap.domain.configurable.warningMessage
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// TODO: Delete with SendFeatureToggles
|
||||
@Deprecated(message = "Used only in old send screen")
|
||||
class WarningMessagesManager {
|
||||
|
||||
private val warningsList = CopyOnWriteArrayList<WarningMessage>()
|
||||
|
||||
fun getWarnings(location: WarningMessage.Location, blockchains: List<Blockchain>): List<WarningMessage> {
|
||||
return warningsList.filter { message ->
|
||||
val messageBlockchains = message.blockchainList
|
||||
val isCorrespondingMessageBlockchains = messageBlockchains == null ||
|
||||
messageBlockchains.any(blockchains::contains)
|
||||
val isCorrespondingMessageLocation = message.location.contains(location)
|
||||
|
||||
!message.isHidden && isCorrespondingMessageLocation && isCorrespondingMessageBlockchains
|
||||
}
|
||||
}
|
||||
|
||||
fun hideWarning(warning: WarningMessage): Boolean {
|
||||
val foundWarning = findWarning(warning) ?: return false
|
||||
val isCorrectType = foundWarning.type == WarningMessage.Type.Temporary ||
|
||||
foundWarning.type == WarningMessage.Type.AppRating
|
||||
|
||||
return if (!foundWarning.isHidden && isCorrectType) {
|
||||
foundWarning.isHidden = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun findWarning(warning: WarningMessage): WarningMessage? {
|
||||
return warningsList.firstOrNull { it == warning }
|
||||
}
|
||||
}
|
||||
|
|
@ -165,10 +165,10 @@ internal class GeneralUserWalletsListManager(
|
|||
}
|
||||
|
||||
if (possibleManager == implementation.value) {
|
||||
Timber.e("Switch to same manager ${possibleManager::class.simpleName}")
|
||||
Timber.e("Switch to the same manager ${possibleManager::class.simpleName}")
|
||||
}
|
||||
|
||||
Timber.d("Switch to ${possibleManager::class.simpleName}")
|
||||
Timber.i("Switch to ${possibleManager::class.simpleName}")
|
||||
|
||||
val previousManager = implementation.value
|
||||
implementation.value = copySelectedUserWallet(
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
application = application,
|
||||
metaData = appMetaData,
|
||||
) { error ->
|
||||
Timber.d("Error while initializing client: $error")
|
||||
Timber.e("Error while initializing client: $error")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
|
|
@ -73,7 +73,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
}
|
||||
|
||||
Web3Wallet.initialize(Wallet.Params.Init(core = CoreClient)) { error ->
|
||||
Timber.d("Error while initializing Web3Wallet: $error")
|
||||
Timber.e("Error while initializing Web3Wallet: $error")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
|
|
@ -94,11 +94,11 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
) {
|
||||
// Triggered when wallet receives the session proposal sent by a Dapp
|
||||
Timber.d("sessionProposal: $sessionProposal")
|
||||
Timber.i("sessionProposal: $sessionProposal")
|
||||
this@DefaultLegacyWalletConnectRepository.sessionProposal = sessionProposal
|
||||
|
||||
if (sessionProposal.name in unsupportedDApps) {
|
||||
Timber.w("Unsupported DApp")
|
||||
Timber.i("Unsupported DApp")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
|
|
@ -115,7 +115,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
)
|
||||
|
||||
if (missingNetworks.isNotEmpty()) {
|
||||
Timber.w("Not added blockchains: $missingNetworks")
|
||||
Timber.i("Not added blockchains: $missingNetworks")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
|
|
@ -150,12 +150,12 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
) {
|
||||
// Triggered when a Dapp sends SessionRequest to sign a transaction or a message
|
||||
Timber.d("sessionRequest: $sessionRequest")
|
||||
Timber.i("sessionRequest: $sessionRequest")
|
||||
val request = wcRequestDeserializer.deserialize(
|
||||
method = sessionRequest.request.method,
|
||||
params = sessionRequest.request.params,
|
||||
)
|
||||
Timber.d("sessionRequestParsed: $request")
|
||||
Timber.i("sessionRequestParsed: $request")
|
||||
|
||||
when (request) {
|
||||
is WcRequest.AddChain -> {
|
||||
|
|
@ -193,7 +193,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
verifyContext: Wallet.Model.VerifyContext,
|
||||
) {
|
||||
// Triggered when Dapp / Requester makes an authorization request
|
||||
Timber.d("onAuthRequest: $authRequest")
|
||||
Timber.i("onAuthRequest: $authRequest")
|
||||
}
|
||||
|
||||
override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {
|
||||
|
|
@ -204,12 +204,12 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
updateSessionsInternal().join()
|
||||
}
|
||||
}
|
||||
Timber.d("onSessionDelete: $sessionDelete")
|
||||
Timber.i("onSessionDelete: $sessionDelete")
|
||||
}
|
||||
|
||||
override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {
|
||||
// Triggered when wallet receives the session settlement response from Dapp
|
||||
Timber.d("onSessionSettleResponse: $settleSessionResponse")
|
||||
Timber.i("onSessionSettleResponse: $settleSessionResponse")
|
||||
if (settleSessionResponse is Wallet.Model.SettledSessionResponse.Result) {
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
|
|
@ -225,19 +225,19 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
|
||||
override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {
|
||||
// Triggered when wallet receives the session update response from Dapp
|
||||
Timber.d("onSessionUpdateResponse: $sessionUpdateResponse")
|
||||
Timber.i("onSessionUpdateResponse: $sessionUpdateResponse")
|
||||
updateSessionsInternal()
|
||||
}
|
||||
|
||||
override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {
|
||||
// Triggered whenever the connection state is changed
|
||||
Timber.d("onConnectionStateChange: $state")
|
||||
Timber.i("onConnectionStateChange: $state")
|
||||
if (state.isAvailable) updateSessionsInternal()
|
||||
}
|
||||
|
||||
override fun onError(error: Wallet.Model.Error) {
|
||||
// Triggered whenever there is an issue inside the SDK
|
||||
Timber.d("onError: $error")
|
||||
Timber.i("onError: $error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -299,12 +299,12 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
},
|
||||
)
|
||||
|
||||
Timber.d("Session approval is prepared for sending: $sessionApproval")
|
||||
Timber.i("Session approval is prepared for sending: $sessionApproval")
|
||||
|
||||
Web3Wallet.approveSession(
|
||||
params = sessionApproval,
|
||||
onSuccess = {
|
||||
Timber.d("Approved successfully: $it")
|
||||
Timber.i("Approved successfully: $it")
|
||||
analyticsHandler.send(
|
||||
WalletConnect.NewSessionEstablished(
|
||||
dAppName = sessionProposal.name,
|
||||
|
|
@ -313,7 +313,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
)
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while approving: $it")
|
||||
Timber.e("Error while approving: $it")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.SessionApprovalError(
|
||||
|
|
@ -370,10 +370,10 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
),
|
||||
),
|
||||
onSuccess = { response ->
|
||||
Timber.d("Session request responded successfully: $response")
|
||||
Timber.i("Session request responded successfully: $response")
|
||||
},
|
||||
onError = { error ->
|
||||
Timber.d(error.throwable, "Error while responging session request")
|
||||
Timber.e(error.throwable, "Error while responging session request")
|
||||
|
||||
WalletConnect.RequestHandledParams(
|
||||
dAppName = session?.name ?: "",
|
||||
|
|
@ -427,10 +427,10 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
reason = "",
|
||||
),
|
||||
onSuccess = {
|
||||
Timber.d("Rejected successfully: $it")
|
||||
Timber.i("Rejected successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while rejecting: $it")
|
||||
Timber.e("Error while rejecting: $it")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -447,10 +447,10 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
),
|
||||
)
|
||||
updateSessionsInternal()
|
||||
Timber.d("Disconnected successfully: $it")
|
||||
Timber.i("Disconnected successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.d("Error while disconnecting: $it")
|
||||
Timber.e("Error while disconnecting: $it")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -483,7 +483,7 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
url = it.metaData?.url,
|
||||
)
|
||||
}
|
||||
Timber.d("Available sessions: $availableSessions")
|
||||
Timber.i("Available sessions: $availableSessions")
|
||||
currentSessions = availableSessions
|
||||
_activeSessions.emit(availableSessions)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class WalletConnectInteractor(
|
|||
val blockchainHelper: WcBlockchainHelper,
|
||||
) {
|
||||
|
||||
var isWalletConnectReadyForDeepLinks = false
|
||||
private var isWalletConnectReadyForDeepLinks = false
|
||||
|
||||
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
GetSelectedWalletUseCase(userWalletsListManager)
|
||||
|
|
@ -82,7 +82,7 @@ class WalletConnectInteractor(
|
|||
|
||||
private suspend fun initWithWallet(userWallet: UserWallet) {
|
||||
if (userWallet.isMultiCurrency) {
|
||||
Timber.d("WalletConnect: initialize and setup networks for ${userWallet.walletId}")
|
||||
Timber.i("WalletConnect: initialize and setup networks for ${userWallet.walletId}")
|
||||
startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet))
|
||||
subscribeOnCurrenciesUpdates(userWallet)
|
||||
}
|
||||
|
|
@ -143,9 +143,9 @@ class WalletConnectInteractor(
|
|||
private suspend fun subscribeToEvents() {
|
||||
events
|
||||
.onEach { wcEvent ->
|
||||
Timber.i("WalletConnect: event: $wcEvent")
|
||||
when (wcEvent) {
|
||||
is WalletConnectEvents.SessionProposal -> {
|
||||
Timber.d("WC session proposal event received")
|
||||
val unsupportedNetworks = wcEvent.requiredChainIds
|
||||
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
|
||||
if (unsupportedNetworks.isNotEmpty()) {
|
||||
|
|
@ -230,6 +230,7 @@ class WalletConnectInteractor(
|
|||
}
|
||||
|
||||
fun approveSessionProposal(accounts: List<Account>) {
|
||||
Timber.i("Approve session proposal: $accounts")
|
||||
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
|
||||
.groupBy { account ->
|
||||
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
|
||||
|
|
@ -241,14 +242,17 @@ class WalletConnectInteractor(
|
|||
}
|
||||
|
||||
fun rejectSessionProposal() {
|
||||
Timber.i("Reject session proposal")
|
||||
walletConnectRepository.reject()
|
||||
}
|
||||
|
||||
fun disconnectSession(topic: String) {
|
||||
Timber.i("Disconnect session: $topic")
|
||||
walletConnectRepository.disconnect(topic)
|
||||
}
|
||||
|
||||
fun cancelRequest(topic: String, id: Long) {
|
||||
Timber.i("Cancel request: $topic, $id")
|
||||
walletConnectRepository.cancelRequest(topic, id)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import arrow.core.getOrElse
|
|||
import arrow.core.raise.result
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
|
|
|
|||
|
|
@ -134,8 +134,6 @@ class DetailsMiddleware {
|
|||
|
||||
scope.launch {
|
||||
repository.changeAppThemeMode(appThemeMode)
|
||||
|
||||
store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.events.IntroductionProcess
|
|||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.home.compose.StoriesScreen
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -26,17 +27,21 @@ import org.rekotlin.StoreSubscriber
|
|||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
||||
internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
lateinit var homeFeatureToggles: HomeFeatureToggles
|
||||
|
||||
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
|
||||
|
||||
private val viewModel by viewModels<HomeViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
store.dispatch(HomeAction.OnCreate)
|
||||
store.dispatch(HomeAction.Init)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -73,17 +78,29 @@ class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
|
|||
StoriesScreen(
|
||||
homeState = homeState,
|
||||
onScanButtonClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonScanCard())
|
||||
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onScanClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonScanCard())
|
||||
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
|
||||
}
|
||||
},
|
||||
onShopButtonClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonBuyCards())
|
||||
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onShopClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonBuyCards())
|
||||
store.dispatch(HomeAction.GoToShop)
|
||||
}
|
||||
},
|
||||
onSearchTokensClick = {
|
||||
Analytics.send(IntroductionProcess.ButtonTokensList())
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens) }
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
if (homeFeatureToggles.isCallbacksRefactoringEnabled) {
|
||||
viewModel.onSearchClick()
|
||||
} else {
|
||||
Analytics.send(IntroductionProcess.ButtonTokensList())
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
131
app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt
Normal file
131
app/src/main/java/com/tangem/tap/features/home/HomeViewModel.kt
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.ShouldSaveAccessCodesUseCase
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.events.Shop
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class HomeViewModel @Inject constructor(
|
||||
private val shouldSaveAccessCodesUseCase: ShouldSaveAccessCodesUseCase,
|
||||
private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel() {
|
||||
|
||||
fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
|
||||
urlOpener.openUrl(NEW_BUY_WALLET_URL)
|
||||
}
|
||||
|
||||
fun onSearchClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(readOnlyContent = true)) }
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
viewModelScope.launch {
|
||||
setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = shouldSaveAccessCodesUseCase())
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Intro,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
|
||||
} else {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Unable to scan card")
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
},
|
||||
onSuccess = ::proceedWithScanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = UserWalletBuilder(
|
||||
scanResponse = scanResponse,
|
||||
generateWalletNameUseCase = generateWalletNameUseCase,
|
||||
).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") },
|
||||
ifRight = {
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
coroutineScope { store.onUserWalletSelected(userWallet = userWallet) }
|
||||
},
|
||||
)
|
||||
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
|
||||
store.dispatchNavigationAction { push(AppRoute.Wallet) }
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
|
||||
if (currency != null) {
|
||||
Analytics.send(
|
||||
event = Basic.SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = Basic.SignedIn.SignInType.Card,
|
||||
walletsCount = "1",
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ import com.tangem.wallet.R
|
|||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun StoriesScreen(
|
||||
internal fun StoriesScreen(
|
||||
homeState: MutableState<HomeState>,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.home.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class HomeFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
|
||||
val isCallbacksRefactoringEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "HOME_SCREEN_CALLBACKS_REFACTORING_ENABLED")
|
||||
}
|
||||
|
|
@ -5,22 +5,17 @@ import org.rekotlin.Action
|
|||
|
||||
sealed class HomeAction : Action {
|
||||
|
||||
object OnCreate : HomeAction()
|
||||
object Init : HomeAction()
|
||||
|
||||
data class InsertStory(val position: Int, val story: Stories) : HomeAction()
|
||||
data object OnCreate : HomeAction()
|
||||
|
||||
/**
|
||||
* Action for scanning card
|
||||
*
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
*/
|
||||
data class ReadCard(
|
||||
val scope: CoroutineScope,
|
||||
) : HomeAction()
|
||||
data class ReadCard(val scope: CoroutineScope) : HomeAction()
|
||||
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
data class GoToShop(val userCountryCode: String?) : HomeAction()
|
||||
data object GoToShop : HomeAction()
|
||||
|
||||
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ import org.rekotlin.Action
|
|||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
internal const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
object HomeMiddleware {
|
||||
val handler = homeMiddleware
|
||||
|
|
@ -51,8 +51,7 @@ private fun handleHomeAction(action: Action) {
|
|||
is HomeAction.OnCreate -> {
|
||||
Analytics.eraseContext()
|
||||
Analytics.send(IntroductionProcess.ScreenOpened())
|
||||
}
|
||||
is HomeAction.Init -> {
|
||||
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
store.dispatch(GlobalAction.FetchUserCountry)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState {
|
|||
|
||||
var state = appState.homeState
|
||||
when (action) {
|
||||
is HomeAction.InsertStory -> {
|
||||
state = state.copy(
|
||||
stories = state.stories.toMutableList().apply {
|
||||
add(action.position, action.story)
|
||||
},
|
||||
)
|
||||
}
|
||||
is HomeAction.ScanInProgress -> {
|
||||
state = state.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
|
||||
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
|
|
@ -18,7 +19,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
|
||||
|
|
@ -37,16 +37,16 @@ internal class MainViewModel @Inject constructor(
|
|||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
private val router: AppRouter,
|
||||
private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase,
|
||||
private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
|
||||
deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
|
||||
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
|
||||
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,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel(), MainIntents {
|
||||
|
||||
|
|
@ -68,7 +68,6 @@ internal class MainViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
||||
|
||||
updateAppCurrencies()
|
||||
updateSendFeatureToggle()
|
||||
observeFlips()
|
||||
displayBalancesHidingStatusToast()
|
||||
displayHiddenBalancesModalNotification()
|
||||
|
|
@ -77,14 +76,14 @@ internal class MainViewModel @Inject constructor(
|
|||
fetchStakingTokens()
|
||||
}
|
||||
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
deleteDeprecatedLogsUseCase()
|
||||
}
|
||||
deleteDeprecatedLogsUseCase()
|
||||
}
|
||||
|
||||
/** Loading the resources needed to run the application */
|
||||
private fun loadApplicationResources() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
apiConfigsManager.initialize()
|
||||
|
||||
blockchainSDKFactory.init()
|
||||
prepareSelectedWalletFeedback()
|
||||
|
||||
|
|
@ -130,12 +129,6 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateSendFeatureToggle() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
sendFeatureToggles.fetchNewSendEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeFlips() {
|
||||
listenToFlipsUseCase().launchIn(viewModelScope)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.extensions.makePrimaryWalletManager
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.utils.popTo
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.extensions.makePrimaryWalletManager
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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]+\$")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = "≈"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import arrow.core.Either
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,16 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal class DefaultExpressAuthProvider(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val configManager: ConfigManager,
|
||||
) : ExpressAuthProvider, ExpressSessionIdGenerator {
|
||||
) : ExpressAuthProvider {
|
||||
|
||||
private var uuid = AtomicReference(UUID.randomUUID())
|
||||
|
||||
override fun getApiKey(): String {
|
||||
return configManager.config.express?.apiKey ?: error("No express api key provided")
|
||||
}
|
||||
|
||||
override fun getUserId(): String {
|
||||
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided")
|
||||
}
|
||||
|
|
@ -25,8 +18,4 @@ internal class DefaultExpressAuthProvider(
|
|||
override fun getSessionId(): String {
|
||||
return uuid.get().toString()
|
||||
}
|
||||
|
||||
override fun generateNewSessionId() {
|
||||
uuid = AtomicReference(UUID.randomUUID())
|
||||
}
|
||||
}
|
||||
|
|
@ -29,14 +29,8 @@ class AuthModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideExpressAuthProvider(
|
||||
userWalletsStore: UserWalletsStore,
|
||||
configManager: ConfigManager,
|
||||
): ExpressAuthProvider {
|
||||
return DefaultExpressAuthProvider(
|
||||
userWalletsStore = userWalletsStore,
|
||||
configManager = configManager,
|
||||
)
|
||||
fun provideExpressAuthProvider(userWalletsStore: UserWalletsStore): ExpressAuthProvider {
|
||||
return DefaultExpressAuthProvider(userWalletsStore = userWalletsStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.network.exchangeServices
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
|
|
|
|||
|
|
@ -96,18 +96,27 @@ class TransactionManagerImpl(
|
|||
// for not EVM blockchains set gasLimit ZERO for now
|
||||
when (fee.data) {
|
||||
is TransactionFee.Single -> {
|
||||
val normalFee = (fee.data as TransactionFee.Single).normal
|
||||
val singleFee = if (normalFee as? Fee.CardanoToken != null) {
|
||||
ProxyFee.CardanoToken(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
minAdaValue = normalFee.minAdaValue,
|
||||
)
|
||||
} else {
|
||||
ProxyFee.Common(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) {
|
||||
is Fee.CardanoToken -> {
|
||||
ProxyFee.CardanoToken(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
minAdaValue = normalFee.minAdaValue,
|
||||
)
|
||||
}
|
||||
is Fee.Filecoin -> {
|
||||
ProxyFee.Filecoin(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
gasPremium = normalFee.gasPremium,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
ProxyFee.Common(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProxyFees.SingleFee(singleFee = singleFee)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.tap.routing.configurator.AppRouterConfig
|
|||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal class ProxyAppRouter(
|
||||
|
|
@ -31,30 +32,35 @@ internal class ProxyAppRouter(
|
|||
|
||||
override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
Timber.i("Push route: $route")
|
||||
innerRouter.push(route, onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
Timber.i("Replace all routes with $routes")
|
||||
innerRouter.replaceAll(*routes, onComplete = onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
Timber.i("Pop route")
|
||||
innerRouter.pop(onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
Timber.i("Pop to route: $route")
|
||||
innerRouter.popTo(route, onComplete)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popTo(routeClass: KClass<out AppRoute>, onComplete: (isSuccess: Boolean) -> Unit) {
|
||||
routerScope.launch(dispatchers.mainImmediate) {
|
||||
Timber.i("Pop to route class: $routeClass")
|
||||
innerRouter.popTo(routeClass, onComplete)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import com.tangem.feature.walletsettings.component.WalletSettingsComponent
|
|||
import com.tangem.features.details.DetailsFeatureToggles
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||
import com.tangem.features.managetokens.ManageTokensToggles
|
||||
import com.tangem.features.managetokens.component.ManageTokensComponent
|
||||
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
|
@ -47,6 +49,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val detailsComponentFactory: DetailsComponent.Factory,
|
||||
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
|
||||
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
|
||||
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
|
||||
private val sendRouter: SendRouter,
|
||||
private val tokenDetailsRouter: TokenDetailsRouter,
|
||||
private val walletRouter: WalletRouter,
|
||||
|
|
@ -55,6 +58,7 @@ internal class ChildFactory @Inject constructor(
|
|||
private val testerRouter: TesterRouter,
|
||||
private val detailsFeatureToggles: DetailsFeatureToggles,
|
||||
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
|
||||
private val manageTokensToggles: ManageTokensToggles,
|
||||
private val pushNotificationRouter: PushNotificationsRouter,
|
||||
) {
|
||||
|
||||
|
|
@ -119,7 +123,21 @@ internal class ChildFactory @Inject constructor(
|
|||
route.asFragmentChild(Provider { HomeFragment() })
|
||||
}
|
||||
is AppRoute.ManageTokens -> {
|
||||
route.asFragmentChild(Provider { TokensListFragment() })
|
||||
if (manageTokensToggles.isFeatureEnabled) {
|
||||
route.asComponentChild(
|
||||
contextProvider = contextProvider(route, contextFactory),
|
||||
params = ManageTokensComponent.Params(
|
||||
mode = if (route.readOnlyContent) {
|
||||
ManageTokensComponent.Mode.READ_ONLY
|
||||
} else {
|
||||
ManageTokensComponent.Mode.MANAGE
|
||||
},
|
||||
),
|
||||
componentFactory = manageTokensComponentFactory,
|
||||
)
|
||||
} else {
|
||||
route.asFragmentChild(Provider { TokensListFragment() })
|
||||
}
|
||||
}
|
||||
is AppRoute.OnboardingNote -> {
|
||||
route.asFragmentChild(Provider { OnboardingNoteFragment() })
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue