Updated on 2026-08-14
This commit is contained in:
commit
0a6b47b668
101 changed files with 1873 additions and 944 deletions
|
|
@ -55,6 +55,7 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.tangem.tap.MainActivity"
|
android:name="com.tangem.tap.MainActivity"
|
||||||
|
android:configChanges="uiMode"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,16 @@ import android.os.Bundle
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import java.util.*
|
import java.util.WeakHashMap
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class ForegroundActivityObserver : ActivityResultCaller {
|
class ForegroundActivityObserver : ActivityResultCaller {
|
||||||
override var activityResultLauncher: ActivityResultLauncher<Intent>? = null
|
override var activityResultLauncher: ActivityResultLauncher<Intent>? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
|
private val activities = WeakHashMap<KClass<out Activity>, AppCompatActivity>()
|
||||||
|
|
||||||
val foregroundActivity: Activity?
|
val foregroundActivity: AppCompatActivity?
|
||||||
get() = activities.entries
|
get() = activities.entries
|
||||||
.filterNot { it.value.isDestroyed }
|
.filterNot { it.value.isDestroyed }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
|
|
@ -35,7 +35,7 @@ class ForegroundActivityObserver : ActivityResultCaller {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
activities[activity::class] = activity
|
activities[activity::class] = activity as? AppCompatActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityDestroyed(activity: Activity) {
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
|
|
@ -59,6 +59,6 @@ class ForegroundActivityObserver : ActivityResultCaller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ForegroundActivityObserver.withForegroundActivity(block: (Activity) -> Unit) {
|
fun ForegroundActivityObserver.withForegroundActivity(block: (AppCompatActivity) -> Unit) {
|
||||||
foregroundActivity?.let { block(it) }
|
foregroundActivity?.let { block(it) }
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.flowWithLifecycle
|
import androidx.lifecycle.flowWithLifecycle
|
||||||
|
|
@ -29,6 +30,7 @@ import com.arkivanov.essenty.lifecycle.asEssentyLifecycle
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
|
import com.tangem.common.routing.AppRouter
|
||||||
import com.tangem.common.routing.entity.SerializableIntent
|
import com.tangem.common.routing.entity.SerializableIntent
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.context.AppComponentContext
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
|
|
@ -38,7 +40,8 @@ import com.tangem.core.navigation.email.EmailSender
|
||||||
import com.tangem.core.ui.event.StateEvent
|
import com.tangem.core.ui.event.StateEvent
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
import com.tangem.core.ui.res.TangemColorPalette
|
||||||
|
import com.tangem.data.card.sdk.CardSdkOwner
|
||||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.card.ScanCardUseCase
|
import com.tangem.domain.card.ScanCardUseCase
|
||||||
import com.tangem.domain.card.repository.CardRepository
|
import com.tangem.domain.card.repository.CardRepository
|
||||||
|
|
@ -114,7 +117,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
lateinit var testerRouter: TesterRouter
|
lateinit var testerRouter: TesterRouter
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver
|
lateinit var cardSdkOwner: CardSdkOwner
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
|
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
|
||||||
|
|
@ -174,6 +177,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
@Inject
|
@Inject
|
||||||
internal lateinit var routingComponentFactory: RoutingComponent.Factory
|
internal lateinit var routingComponentFactory: RoutingComponent.Factory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
internal lateinit var appRouter: AppRouter
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var pushNotificationsRouter: PushNotificationsRouter
|
lateinit var pushNotificationsRouter: PushNotificationsRouter
|
||||||
|
|
||||||
|
|
@ -182,7 +188,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
|
|
||||||
internal val viewModel: MainViewModel by viewModels()
|
internal val viewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||||
|
|
||||||
// TODO: fixme: inject through DI
|
// TODO: fixme: inject through DI
|
||||||
private val intentProcessor: IntentProcessor = IntentProcessor()
|
private val intentProcessor: IntentProcessor = IntentProcessor()
|
||||||
|
|
@ -194,9 +200,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val splashScreen = installSplashScreen()
|
// We need to call it before onCreate to prevent unnecessary activity recreation
|
||||||
|
installAppTheme()
|
||||||
|
|
||||||
installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation
|
val splashScreen = installSplashScreen()
|
||||||
|
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
navigationBarStyle = SystemBarStyle.auto(
|
navigationBarStyle = SystemBarStyle.auto(
|
||||||
|
|
@ -270,7 +277,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun installActivityDependencies() {
|
private fun installActivityDependencies() {
|
||||||
cardSdkLifecycleObserver.onCreate(context = this)
|
cardSdkOwner.register(activity = this)
|
||||||
tangemSdkManager = injectedTangemSdkManager
|
tangemSdkManager = injectedTangemSdkManager
|
||||||
appStateHolder.tangemSdkManager = tangemSdkManager
|
appStateHolder.tangemSdkManager = tangemSdkManager
|
||||||
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
|
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
|
||||||
|
|
@ -301,14 +308,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
|
|
||||||
private fun installAppTheme() {
|
private fun installAppTheme() {
|
||||||
appThemeModeFlow = createAppThemeModeFlow()
|
appThemeModeFlow = createAppThemeModeFlow()
|
||||||
val mode = runBlocking { appThemeModeFlow.filterNotNull().first() }
|
val mode = runBlocking { appThemeModeFlow.first() }
|
||||||
|
|
||||||
updateAppTheme(mode)
|
updateAppTheme(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeAppThemeModeUpdates() {
|
private fun observeAppThemeModeUpdates() {
|
||||||
appThemeModeFlow
|
appThemeModeFlow
|
||||||
.filterNotNull()
|
|
||||||
.flowWithLifecycle(lifecycle)
|
.flowWithLifecycle(lifecycle)
|
||||||
.onEach(::updateAppTheme)
|
.onEach(::updateAppTheme)
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
|
|
@ -324,15 +330,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
|
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
|
||||||
val tangemApplication = application as TangemApplication
|
val tangemApplication = application as TangemApplication
|
||||||
|
|
||||||
return tangemApplication.getAppThemeModeUseCase()
|
return tangemApplication.getAppThemeModeUseCase()
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged()
|
||||||
.map { maybeMode ->
|
.map { maybeMode ->
|
||||||
maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
||||||
}
|
}
|
||||||
.shareIn(
|
.shareIn(
|
||||||
scope = lifecycleScope + Dispatchers.IO,
|
scope = lifecycleScope,
|
||||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +390,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
intentProcessor.removeAll()
|
intentProcessor.removeAll()
|
||||||
cardSdkLifecycleObserver.onDestroy(this)
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,9 +400,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||||
MutableAppThemeModeHolder.value = appThemeMode
|
|
||||||
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
|
||||||
|
|
||||||
val mode = when (appThemeMode) {
|
val mode = when (appThemeMode) {
|
||||||
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
|
@ -403,7 +407,32 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultNightMode(mode)
|
setDefaultNightMode(mode)
|
||||||
delegate.localNightMode = mode
|
|
||||||
|
MutableAppThemeModeHolder.value = appThemeMode
|
||||||
|
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need to manually change the background color of the activity when the UI mode changes to prevent
|
||||||
|
* flickering when navigating between fragments.
|
||||||
|
*
|
||||||
|
|
||||||
|
* `android:configChanges="uiMode"` is set in the manifest.
|
||||||
|
* */
|
||||||
|
updateAppBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateAppBackground() {
|
||||||
|
val backgroundColor = if (isDarkTheme()) {
|
||||||
|
TangemColorPalette.Dark6
|
||||||
|
} else {
|
||||||
|
TangemColorPalette.White
|
||||||
|
}
|
||||||
|
|
||||||
|
findViewById<CoordinatorLayout>(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDarkTheme(): Boolean {
|
private fun isDarkTheme(): Boolean {
|
||||||
|
|
@ -418,12 +447,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
/*
|
|
||||||
* FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called
|
|
||||||
* inside IntentHandler.
|
|
||||||
*/
|
|
||||||
cardSdkLifecycleObserver.onCreate(context = this)
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
intentProcessor.handleIntent(intent, true)
|
intentProcessor.handleIntent(intent, true)
|
||||||
}
|
}
|
||||||
|
|
@ -505,15 +528,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
|
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
|
||||||
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
|
val backStack = appRouter.stack
|
||||||
|
val isOnInitialScreen = backStack.all { it is AppRoute.Welcome || it is AppRoute.Home }
|
||||||
val isNotScannedBefore = store.state.globalState.scanResponse == null
|
val isNotScannedBefore = store.state.globalState.scanResponse == null
|
||||||
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
|
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
|
||||||
|
|
||||||
when {
|
when {
|
||||||
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
!isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||||
navigateToInitialScreen(intentWhichStartedActivity)
|
navigateToInitialScreen(intentWhichStartedActivity)
|
||||||
}
|
}
|
||||||
backStackIsEmpty -> {
|
backStack.isEmpty() -> {
|
||||||
navigateToInitialScreen(intentWhichStartedActivity)
|
navigateToInitialScreen(intentWhichStartedActivity)
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
|
||||||
|
|
@ -367,6 +367,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
||||||
Log.Level.Network,
|
Log.Level.Network,
|
||||||
Log.Level.Error,
|
Log.Level.Error,
|
||||||
Log.Level.Biometric,
|
Log.Level.Biometric,
|
||||||
|
Log.Level.Info,
|
||||||
)
|
)
|
||||||
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
|
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,19 @@ sealed class Settings(
|
||||||
) : Settings("Settings / Card Settings", event, params, error) {
|
) : Settings("Settings / Card Settings", event, params, error) {
|
||||||
|
|
||||||
class ButtonFactoryReset : CardSettings("Button - Factory Reset")
|
class ButtonFactoryReset : CardSettings("Button - Factory Reset")
|
||||||
class FactoryResetFinished(error: Throwable? = null) : CardSettings(
|
class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings(
|
||||||
event = "Factory Reset Finished",
|
event = "Factory Reset Finished",
|
||||||
|
params = buildMap {
|
||||||
|
cardsCount?.let { put("Cards Count", "$it") }
|
||||||
|
},
|
||||||
error = error,
|
error = error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class FactoryResetCanceled(cardsCount: Int) : CardSettings(
|
||||||
|
event = "Factory Reset Canceled",
|
||||||
|
params = mapOf("Cards Count" to "$cardsCount"),
|
||||||
|
)
|
||||||
|
|
||||||
class UserCodeChanged : CardSettings("User Code Changed")
|
class UserCodeChanged : CardSettings("User Code Changed")
|
||||||
class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode")
|
class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode")
|
||||||
|
|
||||||
|
|
|
||||||
168
app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt
Normal file
168
app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.tangem.Log
|
||||||
|
import com.tangem.TangemSdk
|
||||||
|
import com.tangem.common.CardFilter
|
||||||
|
import com.tangem.common.authentication.AuthenticationManager
|
||||||
|
import com.tangem.common.card.FirmwareVersion
|
||||||
|
import com.tangem.common.core.Config
|
||||||
|
import com.tangem.common.services.secure.SecureStorage
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.crypto.bip39.Wordlist
|
||||||
|
import com.tangem.data.card.sdk.CardSdkOwner
|
||||||
|
import com.tangem.data.card.sdk.CardSdkProvider
|
||||||
|
import com.tangem.sdk.DefaultSessionViewDelegate
|
||||||
|
import com.tangem.sdk.extensions.*
|
||||||
|
import com.tangem.sdk.nfc.NfcManager
|
||||||
|
import com.tangem.sdk.storage.create
|
||||||
|
import com.tangem.tap.foregroundActivityObserver
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of CardSDK instance provider
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
internal class DefaultCardSdkProvider @Inject constructor(
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
) : CardSdkProvider, CardSdkOwner {
|
||||||
|
|
||||||
|
private val observer = Observer()
|
||||||
|
|
||||||
|
private var holder: Holder? = null
|
||||||
|
|
||||||
|
override val sdk: TangemSdk
|
||||||
|
get() = holder?.sdk ?: tryToRegisterWithForegroundActivity()
|
||||||
|
|
||||||
|
override fun register(activity: FragmentActivity) {
|
||||||
|
if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) {
|
||||||
|
val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing"
|
||||||
|
analyticsEventHandler.send(TangemSdkWarningEvent(message))
|
||||||
|
Log.info { message }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (holder != null) {
|
||||||
|
unsubscribeAndCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(activity)
|
||||||
|
|
||||||
|
activity.lifecycle.addObserver(observer)
|
||||||
|
|
||||||
|
Log.info { "Tangem SDK owner registered" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tryToRegisterWithForegroundActivity(): TangemSdk {
|
||||||
|
val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity"
|
||||||
|
analyticsEventHandler.send(TangemSdkWarningEvent(warning))
|
||||||
|
Log.warning { warning }
|
||||||
|
|
||||||
|
val activity = foregroundActivityObserver.foregroundActivity
|
||||||
|
|
||||||
|
if (activity == null) {
|
||||||
|
val error = "Tangem SDK holder is null and foreground activity is null"
|
||||||
|
analyticsEventHandler.send(TangemSdkWarningEvent(error))
|
||||||
|
Log.error { error }
|
||||||
|
error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
register(activity)
|
||||||
|
|
||||||
|
val sdk = holder?.sdk
|
||||||
|
|
||||||
|
if (sdk == null) {
|
||||||
|
val error = "Tangem SDK is null after re-registering with foreground activity"
|
||||||
|
analyticsEventHandler.send(TangemSdkWarningEvent(error))
|
||||||
|
Log.error { error }
|
||||||
|
error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialize(activity: FragmentActivity) {
|
||||||
|
val secureStorage = SecureStorage.create(activity)
|
||||||
|
val nfcManager = TangemSdk.initNfcManager(activity)
|
||||||
|
val authenticationManager = TangemSdk.initAuthenticationManager(activity)
|
||||||
|
val keystoreManager = TangemSdk.initKeystoreManager(authenticationManager, secureStorage)
|
||||||
|
|
||||||
|
val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity)
|
||||||
|
viewDelegate.sdkConfig = config
|
||||||
|
|
||||||
|
val sdk = TangemSdk(
|
||||||
|
reader = nfcManager.reader,
|
||||||
|
viewDelegate = viewDelegate,
|
||||||
|
secureStorage = secureStorage,
|
||||||
|
authenticationManager = authenticationManager,
|
||||||
|
keystoreManager = keystoreManager,
|
||||||
|
wordlist = Wordlist.getWordlist(activity),
|
||||||
|
config = config,
|
||||||
|
)
|
||||||
|
|
||||||
|
holder = Holder(
|
||||||
|
activity = activity,
|
||||||
|
nfcManager = nfcManager,
|
||||||
|
authenticationManager = authenticationManager,
|
||||||
|
sdk = sdk,
|
||||||
|
)
|
||||||
|
|
||||||
|
Log.info { "Tangem SDK initialized" }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unsubscribeAndCleanup() {
|
||||||
|
val currentHolder = holder
|
||||||
|
|
||||||
|
if (currentHolder == null) {
|
||||||
|
Log.info { "Tangem SDK already unsubscribed and cleaned up" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
with(currentHolder) {
|
||||||
|
nfcManager.unsubscribe(activity)
|
||||||
|
authenticationManager.unsubscribe(activity)
|
||||||
|
|
||||||
|
activity.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
holder = null
|
||||||
|
|
||||||
|
Log.info { "Tangem SDK unsubscribed and cleaned up" }
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class Observer : DefaultLifecycleObserver {
|
||||||
|
|
||||||
|
override fun onDestroy(owner: LifecycleOwner) {
|
||||||
|
Log.info { "Tangem SDK owner destroyed" }
|
||||||
|
|
||||||
|
unsubscribeAndCleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Holder(
|
||||||
|
val activity: FragmentActivity,
|
||||||
|
val sdk: TangemSdk,
|
||||||
|
val nfcManager: NfcManager,
|
||||||
|
val authenticationManager: AuthenticationManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val config = Config(
|
||||||
|
linkedTerminal = true,
|
||||||
|
allowUntrustedCards = true,
|
||||||
|
filter = CardFilter(
|
||||||
|
allowedCardTypes = FirmwareVersion.FirmwareType.entries.toList(),
|
||||||
|
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
|
||||||
|
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
|
||||||
|
items = setOf("0027", "0030", "0031", "0035"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
|
||||||
|
internal class TangemSdkWarningEvent(message: String) : AnalyticsEvent(
|
||||||
|
category = "Tangem SDK",
|
||||||
|
event = "Warning",
|
||||||
|
error = IllegalStateException(message),
|
||||||
|
)
|
||||||
|
|
@ -33,6 +33,7 @@ internal object CardSDKLoggerModule {
|
||||||
Log.Level.Network,
|
Log.Level.Network,
|
||||||
Log.Level.Error,
|
Log.Level.Error,
|
||||||
Log.Level.Biometric,
|
Log.Level.Biometric,
|
||||||
|
Log.Level.Info,
|
||||||
)
|
)
|
||||||
|
|
||||||
return TangemCardSDKLogger(
|
return TangemCardSDKLogger(
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.tangem.data.card.di
|
package com.tangem.tap.di.data
|
||||||
|
|
||||||
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
|
import com.tangem.data.card.sdk.CardSdkOwner
|
||||||
import com.tangem.data.card.sdk.CardSdkProvider
|
import com.tangem.data.card.sdk.CardSdkProvider
|
||||||
import com.tangem.data.card.sdk.DefaultCardSdkProvider
|
import com.tangem.tap.data.DefaultCardSdkProvider
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -19,5 +19,5 @@ internal interface CardSdkModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver
|
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner
|
||||||
}
|
}
|
||||||
|
|
@ -14,15 +14,15 @@ import com.tangem.tap.domain.sdk.TangemSdkManager
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ViewModelComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(ViewModelComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
internal object CardDomainModule {
|
internal object CardDomainModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideGetBiometricsStatusUseCase(
|
fun provideGetBiometricsStatusUseCase(
|
||||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
): GetBiometricsStatusUseCase {
|
): GetBiometricsStatusUseCase {
|
||||||
|
|
@ -30,7 +30,7 @@ internal object CardDomainModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideSetAccessCodeRequestPolicyUseCase(
|
fun provideSetAccessCodeRequestPolicyUseCase(
|
||||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
): SetAccessCodeRequestPolicyUseCase {
|
): SetAccessCodeRequestPolicyUseCase {
|
||||||
|
|
@ -38,35 +38,35 @@ internal object CardDomainModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
|
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
|
||||||
return WasCardScannedUseCase(cardRepository = cardRepository)
|
return WasCardScannedUseCase(cardRepository = cardRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
|
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
|
||||||
return SetCardWasScannedUseCase(cardRepository = cardRepository)
|
return SetCardWasScannedUseCase(cardRepository = cardRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
|
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
||||||
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
|
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
|
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
|
||||||
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
|
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
||||||
derivationsRepository: DerivationsRepository,
|
derivationsRepository: DerivationsRepository,
|
||||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||||
|
|
@ -74,13 +74,13 @@ internal object CardDomainModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
|
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
|
||||||
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
|
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@Singleton
|
||||||
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
|
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
|
||||||
return DefaultResetCardUseCase(tangemSdkManager)
|
return DefaultResetCardUseCase(tangemSdkManager)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -398,4 +398,16 @@ internal object TokensDomainModule {
|
||||||
stakingRepository = stakingRepository,
|
stakingRepository = stakingRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
|
||||||
|
currenciesRepository: CurrenciesRepository,
|
||||||
|
quotesRepository: QuotesRepository,
|
||||||
|
): RefreshMultiCurrencyWalletQuotesUseCase {
|
||||||
|
return RefreshMultiCurrencyWalletQuotesUseCase(
|
||||||
|
currenciesRepository = currenciesRepository,
|
||||||
|
quotesRepository = quotesRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package com.tangem.tap.domain.card
|
package com.tangem.tap.domain.card
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
|
import arrow.core.raise.Raise
|
||||||
import arrow.core.raise.either
|
import arrow.core.raise.either
|
||||||
import arrow.core.right
|
import arrow.fx.coroutines.ResourceScope
|
||||||
import com.tangem.common.*
|
import arrow.fx.coroutines.resourceScope
|
||||||
|
import com.tangem.common.CompletionResult
|
||||||
|
import com.tangem.common.UserCodeType
|
||||||
import com.tangem.common.core.TangemError
|
import com.tangem.common.core.TangemError
|
||||||
import com.tangem.common.core.TangemSdkError
|
import com.tangem.common.core.TangemSdkError
|
||||||
import com.tangem.common.core.UserCodeRequestPolicy
|
import com.tangem.common.core.UserCodeRequestPolicy
|
||||||
|
|
@ -17,14 +20,14 @@ internal class DefaultResetCardUseCase(
|
||||||
private val tangemSdkManager: TangemSdkManager,
|
private val tangemSdkManager: TangemSdkManager,
|
||||||
) : ResetCardUseCase {
|
) : ResetCardUseCase {
|
||||||
|
|
||||||
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = either {
|
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = resourceScope {
|
||||||
enterRequiredAccessCode(card) {
|
either {
|
||||||
|
withUserCodeRequestPolicy(card)
|
||||||
|
|
||||||
tangemSdkManager.resetToFactorySettings(
|
tangemSdkManager.resetToFactorySettings(
|
||||||
cardId = card.cardId,
|
cardId = card.cardId,
|
||||||
allowsRequestAccessCodeFromRepository = true,
|
allowsRequestAccessCodeFromRepository = true,
|
||||||
)
|
).bind(raise = this)
|
||||||
.doOnSuccess { Unit.right() }
|
|
||||||
.doOnFailure { raise(it.mapToDomainError()) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,23 +35,29 @@ internal class DefaultResetCardUseCase(
|
||||||
cardNumber: Int,
|
cardNumber: Int,
|
||||||
card: CardDTO,
|
card: CardDTO,
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
): Either<ResetCardError, Unit> = either {
|
): Either<ResetCardError, Unit> = resourceScope {
|
||||||
enterRequiredAccessCode(card) {
|
either {
|
||||||
tangemSdkManager.resetBackupCard(cardNumber, userWalletId)
|
withUserCodeRequestPolicy(card)
|
||||||
.doOnSuccess { Unit.right() }
|
|
||||||
.doOnFailure { raise(it.mapToDomainError()) }
|
tangemSdkManager.resetBackupCard(
|
||||||
|
cardNumber = cardNumber,
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
).bind(raise = this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun enterRequiredAccessCode(
|
private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
|
||||||
card: CardDTO,
|
install(
|
||||||
task: suspend TangemSdkManager.() -> CompletionResult<*>,
|
acquire = {
|
||||||
) {
|
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
||||||
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
requestMandatoryAccessCodeEntry(card)
|
||||||
requestMandatoryAccessCodeEntry(card)
|
|
||||||
|
|
||||||
tangemSdkManager.task()
|
policyBeforeReset
|
||||||
.doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) }
|
},
|
||||||
|
release = { prevPolicy, _ ->
|
||||||
|
tangemSdkManager.setUserCodeRequestPolicy(prevPolicy)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
|
private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
|
||||||
|
|
@ -65,7 +74,19 @@ internal class DefaultResetCardUseCase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun CompletionResult<*>.bind(raise: Raise<ResetCardError>) {
|
||||||
|
return when (this) {
|
||||||
|
is CompletionResult.Failure -> {
|
||||||
|
val domainError = error.mapToDomainError()
|
||||||
|
|
||||||
|
raise.raise(domainError)
|
||||||
|
}
|
||||||
|
is CompletionResult.Success -> { /* no-op */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun TangemError.mapToDomainError(): ResetCardError {
|
private fun TangemError.mapToDomainError(): ResetCardError {
|
||||||
return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.AnotherSdkError
|
return if (this is TangemSdkError.UserCancelled) ResetCardError.UserCanceled else ResetCardError.SdkError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWall
|
||||||
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
|
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
|
||||||
|
|
||||||
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
|
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
|
||||||
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build()
|
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return
|
||||||
|
|
||||||
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
|
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,19 +152,29 @@ internal class BiometricUserWalletsListManager(
|
||||||
return CompletionResult.Success(Unit)
|
return CompletionResult.Success(Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
changeSelectedUserWalletIdIfNeeded(idsToRemove)
|
if (idsToRemove.size == state.value.userWallets.size) {
|
||||||
|
return clear()
|
||||||
|
}
|
||||||
|
|
||||||
return sensitiveInformationRepository.delete(idsToRemove)
|
return sensitiveInformationRepository.delete(idsToRemove)
|
||||||
.flatMap { publicInformationRepository.delete(idsToRemove) }
|
.flatMap { publicInformationRepository.delete(idsToRemove) }
|
||||||
.map { keysRepository.delete(idsToRemove) }
|
.map { keysRepository.delete(idsToRemove) }
|
||||||
.map {
|
.map {
|
||||||
state.update { prevState ->
|
state.update { prevState ->
|
||||||
val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
|
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
|
||||||
|
|
||||||
|
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
|
||||||
|
val newSelectedUserWallet = findOrSetSelectedWallet(
|
||||||
|
prevSelectedWalletId = prevState.selectedUserWalletId,
|
||||||
|
userWallets = remainingWallets,
|
||||||
|
ignorePrevSelectedWallet = isSelectedWalletDeleted,
|
||||||
|
)
|
||||||
|
|
||||||
prevState.copy(
|
prevState.copy(
|
||||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
|
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
|
||||||
userWallets = newUserWallets,
|
userWallets = remainingWallets,
|
||||||
isLocked = newUserWallets.any { it.isLocked },
|
isLocked = remainingWallets.any { it.isLocked },
|
||||||
|
selectedUserWalletId = newSelectedUserWallet?.walletId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -173,10 +183,10 @@ internal class BiometricUserWalletsListManager(
|
||||||
override suspend fun clear(): CompletionResult<Unit> {
|
override suspend fun clear(): CompletionResult<Unit> {
|
||||||
return sensitiveInformationRepository.clear()
|
return sensitiveInformationRepository.clear()
|
||||||
.flatMap { publicInformationRepository.clear() }
|
.flatMap { publicInformationRepository.clear() }
|
||||||
.map { keysRepository.clear() }
|
|
||||||
.map {
|
.map {
|
||||||
|
keysRepository.clear()
|
||||||
selectedUserWalletRepository.set(null)
|
selectedUserWalletRepository.set(null)
|
||||||
lock()
|
state.value = State()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,47 +311,24 @@ internal class BiometricUserWalletsListManager(
|
||||||
private fun findOrSetSelectedWallet(
|
private fun findOrSetSelectedWallet(
|
||||||
prevSelectedWalletId: UserWalletId?,
|
prevSelectedWalletId: UserWalletId?,
|
||||||
userWallets: List<UserWallet>,
|
userWallets: List<UserWallet>,
|
||||||
|
ignorePrevSelectedWallet: Boolean = false,
|
||||||
): UserWallet? {
|
): UserWallet? {
|
||||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
var possibleSelectedUserWallet: UserWallet? = null
|
||||||
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
|
||||||
|
if (!ignorePrevSelectedWallet) {
|
||||||
|
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||||
|
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||||
|
}
|
||||||
|
|
||||||
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
|
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
|
||||||
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
|
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
|
||||||
|
|
||||||
if (possibleSelectedUserWallet != null) {
|
|
||||||
selectedUserWalletRepository.set(possibleSelectedUserWallet.walletId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
|
||||||
|
|
||||||
return possibleSelectedUserWallet
|
return possibleSelectedUserWallet
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
|
|
||||||
val remainingWallets = state.value.userWallets.filter {
|
|
||||||
it.walletId !in walletsIdsToRemove
|
|
||||||
}
|
|
||||||
val selectedWallet = findSelectedUserWallet()
|
|
||||||
when {
|
|
||||||
remainingWallets.isEmpty() -> {
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
selectedUserWalletId = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selectedUserWalletRepository.set(null)
|
|
||||||
}
|
|
||||||
!remainingWallets.contains(selectedWallet) -> {
|
|
||||||
val newSelectedWallet = remainingWallets.firstOrNull { !it.isLocked }
|
|
||||||
state.update { prevState ->
|
|
||||||
prevState.copy(
|
|
||||||
selectedUserWalletId = newSelectedWallet?.walletId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
selectedUserWalletRepository.set(newSelectedWallet?.walletId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findSelectedUserWallet(
|
private fun findSelectedUserWallet(
|
||||||
userWallets: List<UserWallet> = state.value.userWallets,
|
userWallets: List<UserWallet> = state.value.userWallets,
|
||||||
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
|
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,5 @@ interface CustomTokenInteractor {
|
||||||
|
|
||||||
/** Save token [customCurrency] */
|
/** Save token [customCurrency] */
|
||||||
@Throws(TangemError::class)
|
@Throws(TangemError::class)
|
||||||
suspend fun saveToken(customCurrency: CustomCurrency)
|
suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.tangem.tap.features.customtoken.impl.domain
|
package com.tangem.tap.features.customtoken.impl.domain
|
||||||
|
|
||||||
|
import arrow.core.getOrElse
|
||||||
|
import arrow.core.raise.result
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||||
|
|
@ -15,7 +17,6 @@ import com.tangem.tap.domain.model.Currency
|
||||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of custom token interactor
|
* Default implementation of custom token interactor
|
||||||
|
|
@ -45,16 +46,18 @@ class DefaultCustomTokenInteractor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveToken(customCurrency: CustomCurrency) {
|
override suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit> {
|
||||||
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
|
return result {
|
||||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
|
||||||
|
error("Failed to get selected wallet: $it")
|
||||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
|
||||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
|
||||||
.onRight {
|
|
||||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
|
||||||
}
|
}
|
||||||
.onLeft { Timber.e("Failed to derive public keys: $it") }
|
|
||||||
|
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||||
|
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||||
|
|
||||||
|
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
|
||||||
|
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
|
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.features.customtoken.impl.presentation
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.systemBarsPadding
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.tangem.core.ui.UiDependencies
|
import com.tangem.core.ui.UiDependencies
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.screen.ComposeFragment
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
|
@ -30,11 +32,13 @@ internal class AddCustomTokenFragment : ComposeFragment() {
|
||||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
|
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
|
||||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||||
}
|
}
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
AddCustomTokenScreen(
|
AddCustomTokenScreen(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(TangemTheme.colors.background.primary)
|
.background(TangemTheme.colors.background.primary)
|
||||||
.systemBarsPadding(),
|
.systemBarsPadding(),
|
||||||
stateHolder = viewModel.uiState,
|
stateHolder = state,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -26,8 +26,8 @@ object ContractAddressValidator {
|
||||||
Blockchain.Unknown,
|
Blockchain.Unknown,
|
||||||
Blockchain.Binance,
|
Blockchain.Binance,
|
||||||
Blockchain.BinanceTestnet,
|
Blockchain.BinanceTestnet,
|
||||||
Blockchain.Cardano,
|
|
||||||
-> SuccessAddressValidator.validate(address)
|
-> SuccessAddressValidator.validate(address)
|
||||||
|
Blockchain.Cardano -> blockchain.validateContractAddress(address)
|
||||||
else -> blockchain.validateAddress(address)
|
else -> blockchain.validateAddress(address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
|
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
|
||||||
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter
|
||||||
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
|
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.blockchain.common.Blockchain
|
||||||
import com.tangem.blockchain.common.Token
|
import com.tangem.blockchain.common.Token
|
||||||
|
|
@ -48,6 +46,9 @@ import com.tangem.wallet.R
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.flow.updateAndGet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -79,12 +80,12 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
private val testActionsHandler = TestActionsHandler()
|
private val testActionsHandler = TestActionsHandler()
|
||||||
private val formStateBuilder = FormStateBuilder()
|
private val formStateBuilder = FormStateBuilder()
|
||||||
private val hederaAddressConverter = HederaTokenAddressConverter()
|
private val hederaAddressConverter = HederaTokenAddressConverter()
|
||||||
|
private val cardanoTokenAddressConverter = CardanoTokenAddressConverter()
|
||||||
|
|
||||||
private var currentCryptoCurrencies: List<CryptoCurrency> = emptyList()
|
private var currentCryptoCurrencies: List<CryptoCurrency> = emptyList()
|
||||||
|
|
||||||
/** Screen state */
|
/** Screen state */
|
||||||
var uiState by mutableStateOf(getInitialUiState())
|
val uiState: MutableStateFlow<AddCustomTokenStateHolder> = MutableStateFlow(value = getInitialUiState())
|
||||||
private set
|
|
||||||
|
|
||||||
private var foundToken: FoundToken? = null
|
private var foundToken: FoundToken? = null
|
||||||
|
|
||||||
|
|
@ -368,48 +369,54 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
runCatching(dispatchers.io) {
|
runCatching(dispatchers.io) {
|
||||||
val tokenAddress = convertTokenAddress(selectedNetwork, address)
|
val tokenAddress = convertTokenAddress(selectedNetwork, address)
|
||||||
|
?: error("TokenAddress is invalid")
|
||||||
|
|
||||||
featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork)
|
featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork)
|
||||||
}
|
}
|
||||||
.onSuccess { token ->
|
.onSuccess { token ->
|
||||||
foundToken = token
|
foundToken = token
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
form = state.form.copy(
|
||||||
isLoading = false,
|
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||||
),
|
isLoading = false,
|
||||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
),
|
||||||
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
networkSelectorField = state.form.networkSelectorField.copy(
|
||||||
blockchain = Blockchain.fromNetworkId(token.network.id)
|
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
||||||
?: Blockchain.Unknown,
|
blockchain = Blockchain.fromNetworkId(token.network.id)
|
||||||
|
?: Blockchain.Unknown,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenNameInputField = state.form.tokenNameInputField.copy(
|
||||||
|
value = token.name,
|
||||||
|
isEnabled = false,
|
||||||
|
),
|
||||||
|
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(
|
||||||
|
value = token.symbol,
|
||||||
|
isEnabled = false,
|
||||||
|
),
|
||||||
|
decimalsInputField = state.form.decimalsInputField.copy(
|
||||||
|
value = token.network.decimalCount,
|
||||||
|
isEnabled = false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(
|
)
|
||||||
value = token.name,
|
}
|
||||||
isEnabled = false,
|
|
||||||
),
|
|
||||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
|
||||||
value = token.symbol,
|
|
||||||
isEnabled = false,
|
|
||||||
),
|
|
||||||
decimalsInputField = uiState.form.decimalsInputField.copy(
|
|
||||||
value = token.network.decimalCount,
|
|
||||||
isEnabled = false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.onFailure {
|
.onFailure {
|
||||||
foundToken = null
|
foundToken = null
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
form = state.form.copy(
|
||||||
isLoading = false,
|
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||||
|
isLoading = false,
|
||||||
|
),
|
||||||
|
tokenNameInputField = state.form.tokenNameInputField.copy(isEnabled = true),
|
||||||
|
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(isEnabled = true),
|
||||||
|
decimalsInputField = state.form.decimalsInputField.copy(isEnabled = true),
|
||||||
),
|
),
|
||||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(isEnabled = true),
|
)
|
||||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(isEnabled = true),
|
}
|
||||||
decimalsInputField = uiState.form.decimalsInputField.copy(isEnabled = true),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,28 +426,31 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isDerivationPathSelected(): Boolean {
|
private fun isDerivationPathSelected(): Boolean {
|
||||||
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
|
val blockchain = uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain
|
||||||
this != null && this != Blockchain.Unknown ||
|
val selectorType = uiState.value.form.derivationPathSelectorField?.selectedItem?.type
|
||||||
uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM &&
|
|
||||||
!uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
return blockchain != null && blockchain != Blockchain.Unknown ||
|
||||||
}
|
selectorType == DerivationPathSelectorType.CUSTOM &&
|
||||||
|
!uiState.value.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateWarnings() {
|
private fun updateWarnings() {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
warnings = buildSet {
|
state.copySealed(
|
||||||
when (getCustomTokenType()) {
|
warnings = buildSet {
|
||||||
CustomTokenType.TOKEN -> {
|
when (getCustomTokenType()) {
|
||||||
addAll(getTokenWarningSet())
|
CustomTokenType.TOKEN -> {
|
||||||
}
|
addAll(getTokenWarningSet())
|
||||||
|
}
|
||||||
|
|
||||||
CustomTokenType.BLOCKCHAIN -> {
|
CustomTokenType.BLOCKCHAIN -> {
|
||||||
if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded)
|
if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||||
if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken)
|
if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCustomTokenType(): CustomTokenType {
|
private fun getCustomTokenType(): CustomTokenType {
|
||||||
|
|
@ -452,24 +462,24 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAnyTokenFieldsFilled(): Boolean {
|
private fun isAnyTokenFieldsFilled(): Boolean {
|
||||||
return with(uiState.form) {
|
return with(uiState.value.form) {
|
||||||
contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() ||
|
contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() ||
|
||||||
tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty()
|
tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAllTokenFieldsFilled(): Boolean {
|
private fun isAllTokenFieldsFilled(): Boolean {
|
||||||
return with(uiState.form) {
|
return with(uiState.value.form) {
|
||||||
contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() &&
|
contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() &&
|
||||||
tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty()
|
tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
||||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||||
|
|
||||||
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
||||||
address = uiState.form.contractAddressInputField.value,
|
address = uiState.value.form.contractAddressInputField.value,
|
||||||
blockchain = networkSelectorValue,
|
blockchain = networkSelectorValue,
|
||||||
).let {
|
).let {
|
||||||
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
|
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
|
||||||
|
|
@ -503,66 +513,70 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNetworkSelected(): Boolean {
|
private fun isNetworkSelected(): Boolean {
|
||||||
return uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFloatingButton() {
|
private fun updateFloatingButton() {
|
||||||
uiState = updateStateWithDerivationError(uiState)
|
uiState.update { state ->
|
||||||
|
updateStateWithDerivationError(state)
|
||||||
|
}
|
||||||
if (isCustomTokenAlreadyAdded()) {
|
if (isCustomTokenAlreadyAdded()) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
state.copySealed(
|
||||||
floatingButton = uiState.floatingButton.copy(isEnabled = false),
|
warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||||
)
|
floatingButton = state.floatingButton.copy(isEnabled = false),
|
||||||
|
)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
uiState.update { state ->
|
||||||
val state = when {
|
val isCorrectDerivationInput = !state.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
val updatedState = when {
|
||||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||||
val error = ContractAddressValidator.validate(
|
val networkSelectorValue = state.form.networkSelectorField.selectedItem.blockchain
|
||||||
address = uiState.form.contractAddressInputField.value,
|
val error = ContractAddressValidator.validate(
|
||||||
blockchain = networkSelectorValue,
|
address = state.form.contractAddressInputField.value,
|
||||||
)
|
blockchain = networkSelectorValue,
|
||||||
|
)
|
||||||
|
|
||||||
val isSupportedToken = getSelectedWalletSyncUseCase().fold(
|
val isSupportedToken = getSelectedWalletSyncUseCase().fold(
|
||||||
ifLeft = { false },
|
ifLeft = { false },
|
||||||
ifRight = {
|
ifRight = {
|
||||||
it.scanResponse.card.canHandleToken(
|
it.scanResponse.card.canHandleToken(
|
||||||
blockchain = networkSelectorValue,
|
blockchain = networkSelectorValue,
|
||||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||||
)
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
uiState.copySealed(
|
|
||||||
floatingButton = uiState.floatingButton.copy(
|
|
||||||
isEnabled = error is ContractAddressValidatorResult.Success &&
|
|
||||||
isSupportedToken && isCorrectDerivationInput,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
isAnyTokenFieldsFilled() -> {
|
|
||||||
uiState.copySealed(floatingButton = uiState.floatingButton.copy(isEnabled = false))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
uiState.copySealed(
|
|
||||||
floatingButton = uiState.floatingButton.copy(
|
|
||||||
isEnabled = if (isNetworkSelected()) {
|
|
||||||
!isBlockchainAlreadyAdded() && isCorrectDerivationInput
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uiState = state.copySealed(
|
state.copySealed(
|
||||||
warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
floatingButton = state.floatingButton.copy(
|
||||||
)
|
isEnabled = error is ContractAddressValidatorResult.Success &&
|
||||||
|
isSupportedToken && isCorrectDerivationInput,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isAnyTokenFieldsFilled() -> {
|
||||||
|
state.copySealed(floatingButton = state.floatingButton.copy(isEnabled = false))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
state.copySealed(
|
||||||
|
floatingButton = state.floatingButton.copy(
|
||||||
|
isEnabled = if (isNetworkSelected()) {
|
||||||
|
!isBlockchainAlreadyAdded() && isCorrectDerivationInput
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedState.copySealed(
|
||||||
|
warnings = updatedState.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder {
|
private fun updateStateWithDerivationError(uiState: AddCustomTokenStateHolder): AddCustomTokenStateHolder {
|
||||||
|
|
@ -595,13 +609,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isTokenAlreadyAdded(): Boolean {
|
private fun isTokenAlreadyAdded(): Boolean {
|
||||||
|
val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||||
|
val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id
|
||||||
|
val contractAddress = convertTokenAddress(
|
||||||
|
blockchain = networkSelectorValue,
|
||||||
|
address = uiState.value.form.contractAddressInputField.value,
|
||||||
|
) ?: return false // invalid address can't be "already added"
|
||||||
|
|
||||||
return currentCryptoCurrencies
|
return currentCryptoCurrencies
|
||||||
.filterIsInstance<CryptoCurrency.Token>()
|
.filterIsInstance<CryptoCurrency.Token>()
|
||||||
.any { token ->
|
.any { token ->
|
||||||
val contractAddress = uiState.form.contractAddressInputField.value
|
|
||||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
|
||||||
val networkId = Blockchain.fromNetworkId(networkSelectorValue.toNetworkId())?.id
|
|
||||||
|
|
||||||
val sameId = if (!token.isCustom) {
|
val sameId = if (!token.isCustom) {
|
||||||
// todo after move foundToken to CryptoCurrency model, use only id
|
// todo after move foundToken to CryptoCurrency model, use only id
|
||||||
foundToken?.id == token.id.rawCurrencyId
|
foundToken?.id == token.id.rawCurrencyId
|
||||||
|
|
@ -621,7 +638,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
return currentCryptoCurrencies
|
return currentCryptoCurrencies
|
||||||
.filterIsInstance<CryptoCurrency.Coin>()
|
.filterIsInstance<CryptoCurrency.Coin>()
|
||||||
.any { coin ->
|
.any { coin ->
|
||||||
coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id &&
|
coin.network.id.value == uiState.value.form.networkSelectorField.selectedItem.blockchain.id &&
|
||||||
coin.network.derivationPath.value == getDerivationPath()?.rawPath
|
coin.network.derivationPath.value == getDerivationPath()?.rawPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -630,39 +647,43 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
when {
|
when {
|
||||||
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
||||||
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
|
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
form = state.form.copy(
|
||||||
isError = true,
|
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||||
error = TextReference.Res(
|
isError = true,
|
||||||
id = R.string.custom_token_creation_error_invalid_contract_address,
|
error = TextReference.Res(
|
||||||
|
id = R.string.custom_token_creation_error_invalid_contract_address,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenNameInputField = state.form.tokenNameInputField.copy(
|
||||||
|
isEnabled = isAnotherTokenFieldsFilled,
|
||||||
|
),
|
||||||
|
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(
|
||||||
|
isEnabled = isAnotherTokenFieldsFilled,
|
||||||
|
),
|
||||||
|
decimalsInputField = state.form.decimalsInputField.copy(
|
||||||
|
isEnabled = isAnotherTokenFieldsFilled,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(
|
)
|
||||||
isEnabled = isAnotherTokenFieldsFilled,
|
}
|
||||||
),
|
|
||||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
|
||||||
isEnabled = isAnotherTokenFieldsFilled,
|
|
||||||
),
|
|
||||||
decimalsInputField = uiState.form.decimalsInputField.copy(
|
|
||||||
isEnabled = isAnotherTokenFieldsFilled,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
|
!isNetworkSelected() || type == AddCustomTokenError.FIELD_IS_EMPTY -> {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
|
form = state.form.copy(
|
||||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = "", isEnabled = false),
|
contractAddressInputField = state.form.contractAddressInputField.copy(isError = false),
|
||||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
tokenNameInputField = state.form.tokenNameInputField.copy(value = "", isEnabled = false),
|
||||||
value = "",
|
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(
|
||||||
isEnabled = false,
|
value = "",
|
||||||
|
isEnabled = false,
|
||||||
|
),
|
||||||
|
decimalsInputField = state.form.decimalsInputField.copy(value = "", isEnabled = false),
|
||||||
),
|
),
|
||||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = "", isEnabled = false),
|
)
|
||||||
),
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
@ -670,11 +691,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDerivationPath(): DerivationPath? {
|
private fun getDerivationPath(): DerivationPath? {
|
||||||
return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) {
|
return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||||
DerivationPathSelectorType.CUSTOM ->
|
DerivationPathSelectorType.CUSTOM ->
|
||||||
createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "")
|
createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "")
|
||||||
else ->
|
else ->
|
||||||
getDerivationPathForBlockchain(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain)
|
getDerivationPathForBlockchain(uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -697,7 +718,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
|
val derivationNetwork = if (blockchain == Blockchain.Unknown) {
|
||||||
uiState.form.networkSelectorField.selectedItem.blockchain
|
uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||||
} else {
|
} else {
|
||||||
blockchain
|
blockchain
|
||||||
}
|
}
|
||||||
|
|
@ -732,13 +753,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onContactAddressValueChange(enteredValue: String) {
|
fun onContactAddressValueChange(enteredValue: String) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(value = enteredValue),
|
form = state.form.copy(
|
||||||
),
|
contractAddressInputField = state.form.contractAddressInputField.copy(value = enteredValue),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
|
val selectedNetwork = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||||
val validatorResult = ContractAddressValidator.validate(
|
val validatorResult = ContractAddressValidator.validate(
|
||||||
address = enteredValue,
|
address = enteredValue,
|
||||||
blockchain = selectedNetwork,
|
blockchain = selectedNetwork,
|
||||||
|
|
@ -746,14 +769,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
|
|
||||||
when (validatorResult) {
|
when (validatorResult) {
|
||||||
is ContractAddressValidatorResult.Success -> {
|
is ContractAddressValidatorResult.Success -> {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
form = state.form.copy(
|
||||||
isError = false,
|
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||||
isLoading = true,
|
isError = false,
|
||||||
|
isLoading = true,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
}
|
||||||
updateForm(address = enteredValue, selectedNetwork = selectedNetwork)
|
updateForm(address = enteredValue, selectedNetwork = selectedNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -766,76 +791,92 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onNetworkSelectorItemClick(index: Int) {
|
fun onNetworkSelectorItemClick(index: Int) {
|
||||||
val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index))
|
val state = uiState.updateAndGet { state ->
|
||||||
uiState = uiState.copySealed(
|
val selectedItem = requireNotNull(state.form.networkSelectorField.items.getOrNull(index))
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
form = state.form.copy(
|
||||||
selectedItem = selectedItem,
|
networkSelectorField = state.form.networkSelectorField.copy(
|
||||||
|
selectedItem = selectedItem,
|
||||||
|
),
|
||||||
|
showTokenFields = selectedItem.blockchain.canHandleTokens() &&
|
||||||
|
// workaround cause in Terra we support only 1 token
|
||||||
|
selectedItem.blockchain != Blockchain.TerraV1,
|
||||||
),
|
),
|
||||||
showTokenFields = selectedItem.blockchain.canHandleTokens() &&
|
)
|
||||||
// workaround cause in Terra we support only 1 token
|
}
|
||||||
selectedItem.blockchain != Blockchain.TerraV1,
|
|
||||||
),
|
onContactAddressValueChange(state.form.contractAddressInputField.value)
|
||||||
)
|
|
||||||
onContactAddressValueChange(uiState.form.contractAddressInputField.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTokenNameValueChange(enteredValue: String) {
|
fun onTokenNameValueChange(enteredValue: String) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue),
|
form = state.form.copy(
|
||||||
),
|
tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
updateFloatingButton()
|
updateFloatingButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTokenSymbolValueChange(enteredValue: String) {
|
fun onTokenSymbolValueChange(enteredValue: String) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue),
|
form = state.form.copy(
|
||||||
),
|
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
updateFloatingButton()
|
updateFloatingButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDecimalsValueChange(enteredValue: String) {
|
fun onDecimalsValueChange(enteredValue: String) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue),
|
form = state.form.copy(
|
||||||
),
|
decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
updateFloatingButton()
|
updateFloatingButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDerivationPathSelectorItemClick(index: Int) {
|
fun onDerivationPathSelectorItemClick(index: Int) {
|
||||||
val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField)
|
uiState.update { state ->
|
||||||
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
val derivationSelector = requireNotNull(state.form.derivationPathSelectorField)
|
||||||
val derivationInputField = requireNotNull(uiState.form.derivationPathInputField)
|
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
||||||
uiState = uiState.copySealed(
|
val derivationInputField = requireNotNull(state.form.derivationPathInputField)
|
||||||
form = uiState.form.copy(
|
|
||||||
derivationPathSelectorField = derivationSelector.copy(
|
state.copySealed(
|
||||||
selectedItem = selected,
|
form = state.form.copy(
|
||||||
|
derivationPathSelectorField = derivationSelector.copy(
|
||||||
|
selectedItem = selected,
|
||||||
|
),
|
||||||
|
derivationPathInputField = derivationInputField.copy(
|
||||||
|
showField = selected.type == DerivationPathSelectorType.CUSTOM,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
derivationPathInputField = derivationInputField.copy(
|
)
|
||||||
showField = selected.type == DerivationPathSelectorType.CUSTOM,
|
}
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
updateFloatingButton()
|
updateFloatingButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDerivationPathValueChange(enteredValue: String) {
|
fun onDerivationPathValueChange(enteredValue: String) {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue),
|
form = state.form.copy(
|
||||||
),
|
derivationPathInputField = state.form.derivationPathInputField?.copy(value = enteredValue),
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
updateFloatingButton()
|
updateFloatingButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAddCustomTokenClick() {
|
fun onAddCustomTokenClick() {
|
||||||
if (!isNetworkSelected()) return
|
if (!isNetworkSelected()) return
|
||||||
val blockchain = uiState.form.networkSelectorField.selectedItem.blockchain
|
val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||||
when (getSupportBlockchainType(blockchain)) {
|
when (getSupportBlockchainType(blockchain)) {
|
||||||
SupportBlockchainType.SUPPORTED -> {
|
SupportBlockchainType.SUPPORTED -> {
|
||||||
/* no-op */
|
/* no-op */
|
||||||
|
|
@ -854,14 +895,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
CustomTokenType.TOKEN -> {
|
CustomTokenType.TOKEN -> {
|
||||||
val contractAddress = convertTokenAddress(
|
val contractAddress = convertTokenAddress(
|
||||||
blockchain = blockchain,
|
blockchain = blockchain,
|
||||||
address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value,
|
address = foundToken?.network?.contractAddress ?: uiState.value.form.contractAddressInputField
|
||||||
)
|
.value,
|
||||||
|
) ?: error("Contract address is invalid") // impossible to add a token with invalid address
|
||||||
|
|
||||||
CustomCurrency.CustomToken(
|
CustomCurrency.CustomToken(
|
||||||
token = Token(
|
token = Token(
|
||||||
name = uiState.form.tokenNameInputField.value,
|
name = uiState.value.form.tokenNameInputField.value,
|
||||||
symbol = uiState.form.tokenSymbolInputField.value,
|
symbol = uiState.value.form.tokenSymbolInputField.value,
|
||||||
contractAddress = contractAddress,
|
contractAddress = contractAddress,
|
||||||
decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()),
|
decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()),
|
||||||
id = foundToken?.id,
|
id = foundToken?.id,
|
||||||
),
|
),
|
||||||
network = blockchain,
|
network = blockchain,
|
||||||
|
|
@ -878,71 +921,101 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
||||||
|
|
||||||
analyticsSender.sendWhenAddTokenButtonClicked(currency)
|
analyticsSender.sendWhenAddTokenButtonClicked(currency)
|
||||||
|
|
||||||
viewModelScope.launch(dispatchers.io) {
|
viewModelScope.launch {
|
||||||
val oldButtonState = uiState.floatingButton
|
uiState.update { state ->
|
||||||
uiState = uiState.copySealed(
|
state.copySealed(
|
||||||
floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true),
|
floatingButton = state.floatingButton.copy(
|
||||||
)
|
isEnabled = false,
|
||||||
runCatching { featureInteractor.saveToken(currency) }
|
showProgress = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = featureInteractor.saveToken(currency)
|
||||||
|
|
||||||
|
uiState.update { state ->
|
||||||
|
state.copySealed(
|
||||||
|
floatingButton = state.floatingButton.copy(
|
||||||
|
isEnabled = true,
|
||||||
|
showProgress = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
.onSuccess { featureRouter.openWalletScreen() }
|
.onSuccess { featureRouter.openWalletScreen() }
|
||||||
.onFailure {
|
.onFailure { Timber.e(it, "Unable to save custom token") }
|
||||||
uiState = uiState.copySealed(floatingButton = oldButtonState)
|
|
||||||
Timber.e(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertTokenAddress(blockchain: Blockchain, address: String): String {
|
/** Convert [address] to single address for specific [blockchain] or return null if invalid */
|
||||||
return when (blockchain) {
|
private fun convertTokenAddress(blockchain: Blockchain, address: String): String? {
|
||||||
Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address)
|
return runCatching {
|
||||||
else -> address
|
when (blockchain) {
|
||||||
|
Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address)
|
||||||
|
Blockchain.Cardano -> {
|
||||||
|
// TODO: [REDACTED_JIRA]
|
||||||
|
cardanoTokenAddressConverter.convertToFingerprint(
|
||||||
|
address = address,
|
||||||
|
symbol = uiState.value.form.tokenSymbolInputField.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> address
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class TestActionsHandler {
|
private inner class TestActionsHandler {
|
||||||
|
|
||||||
fun onClearAddressButtonClick() {
|
fun onClearAddressButtonClick() {
|
||||||
uiState = uiState.copySealed(
|
uiState.update { state ->
|
||||||
form = uiState.form.copy(
|
state.copySealed(
|
||||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
form = state.form.copy(
|
||||||
value = "",
|
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||||
isLoading = false,
|
|
||||||
isError = false,
|
|
||||||
error = null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onResetButtonClick() {
|
|
||||||
with(uiState.form) {
|
|
||||||
uiState = uiState.copySealed(
|
|
||||||
form = uiState.form.copy(
|
|
||||||
contractAddressInputField = contractAddressInputField.copy(
|
|
||||||
value = "",
|
value = "",
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
isError = false,
|
isError = false,
|
||||||
error = null,
|
error = null,
|
||||||
),
|
),
|
||||||
networkSelectorField = networkSelectorField.copy(
|
|
||||||
selectedItem = formStateBuilder.createNetworkSelectorItem(blockchain = Blockchain.Unknown),
|
|
||||||
),
|
|
||||||
tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false),
|
|
||||||
tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false),
|
|
||||||
decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
|
|
||||||
derivationPathSelectorField = derivationPathSelectorField?.copy(
|
|
||||||
isEnabled = true,
|
|
||||||
selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem(
|
|
||||||
blockchain = Blockchain.Unknown,
|
|
||||||
type = DerivationPathSelectorType.DEFAULT,
|
|
||||||
derivationPath = "",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onResetButtonClick() {
|
||||||
|
uiState.update { state ->
|
||||||
|
with(state.form) {
|
||||||
|
state.copySealed(
|
||||||
|
form = state.form.copy(
|
||||||
|
contractAddressInputField = contractAddressInputField.copy(
|
||||||
|
value = "",
|
||||||
|
isLoading = false,
|
||||||
|
isError = false,
|
||||||
|
error = null,
|
||||||
|
),
|
||||||
|
networkSelectorField = networkSelectorField.copy(
|
||||||
|
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
||||||
|
blockchain = Blockchain.Unknown,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false),
|
||||||
|
tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false),
|
||||||
|
decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
|
||||||
|
derivationPathSelectorField = derivationPathSelectorField?.copy(
|
||||||
|
isEnabled = true,
|
||||||
|
selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem(
|
||||||
|
blockchain = Blockchain.Unknown,
|
||||||
|
type = DerivationPathSelectorType.DEFAULT,
|
||||||
|
derivationPath = "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.common.CardTypesResolver
|
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
||||||
|
|
@ -33,7 +31,8 @@ sealed class DetailsAction : Action {
|
||||||
|
|
||||||
data object ScanCard : DetailsAction()
|
data object ScanCard : DetailsAction()
|
||||||
|
|
||||||
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
|
data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction()
|
||||||
|
|
||||||
data object ResetCardSettingsData : DetailsAction()
|
data object ResetCardSettingsData : DetailsAction()
|
||||||
data object ScanAndSaveUserWallet : DetailsAction() {
|
data object ScanAndSaveUserWallet : DetailsAction() {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||||
|
|
@ -148,7 +147,7 @@ class DetailsMiddleware {
|
||||||
}
|
}
|
||||||
.doOnFailure { error ->
|
.doOnFailure { error ->
|
||||||
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
|
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
|
||||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
|
Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.doOnResult {
|
.doOnResult {
|
||||||
|
|
@ -474,10 +473,7 @@ class DetailsMiddleware {
|
||||||
|
|
||||||
if (isSameWallet) {
|
if (isSameWallet) {
|
||||||
store.dispatchOnMain(
|
store.dispatchOnMain(
|
||||||
DetailsAction.PrepareCardSettingsData(
|
DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
|
||||||
scanResponse.card,
|
|
||||||
scanResponse.cardTypesResolver,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
store.dispatchDialogShow(
|
store.dispatchDialogShow(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.common.CardTypesResolver
|
import com.tangem.domain.common.CardTypesResolver
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.tap.common.extensions.inject
|
import com.tangem.tap.common.extensions.inject
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.domain.extensions.signedHashesCount
|
import com.tangem.tap.domain.extensions.signedHashesCount
|
||||||
|
|
@ -29,11 +30,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
||||||
handlePrepareScreen(action, state)
|
handlePrepareScreen(action, state)
|
||||||
}
|
}
|
||||||
is DetailsAction.PrepareCardSettingsData -> {
|
is DetailsAction.PrepareCardSettingsData -> {
|
||||||
handlePrepareCardSettingsScreen(
|
handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
|
||||||
card = action.card,
|
|
||||||
cardTypesResolver = action.cardTypesResolver,
|
|
||||||
state = detailsState,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
|
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
|
||||||
is DetailsAction.ResetToFactory -> {
|
is DetailsAction.ResetToFactory -> {
|
||||||
|
|
@ -99,17 +96,17 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handlePrepareCardSettingsScreen(
|
private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
|
||||||
card: CardDTO,
|
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||||
cardTypesResolver: CardTypesResolver,
|
val card = scanResponse.card
|
||||||
state: DetailsState,
|
|
||||||
): DetailsState {
|
|
||||||
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
|
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
|
||||||
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
|
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
|
||||||
|
|
||||||
val cardSettingsState = CardSettingsState(
|
val cardSettingsState = CardSettingsState(
|
||||||
cardInfo = card.toCardInfo(cardTypesResolver),
|
cardInfo = card.toCardInfo(cardTypesResolver),
|
||||||
|
scanResponse = scanResponse,
|
||||||
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
|
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
|
||||||
card = card,
|
card = scanResponse.card,
|
||||||
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
|
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
|
||||||
resetButtonEnabled = false,
|
resetButtonEnabled = false,
|
||||||
condition1Checked = false,
|
condition1Checked = false,
|
||||||
|
|
@ -126,6 +123,7 @@ private fun handlePrepareCardSettingsScreen(
|
||||||
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
|
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
return state.copy(cardSettingsState = cardSettingsState)
|
return state.copy(cardSettingsState = cardSettingsState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ data class AccessCodeRecoveryState(
|
||||||
data class CardSettingsState(
|
data class CardSettingsState(
|
||||||
val cardInfo: CardInfo,
|
val cardInfo: CardInfo,
|
||||||
val card: CardDTO,
|
val card: CardDTO,
|
||||||
|
val scanResponse: ScanResponse,
|
||||||
val manageSecurityState: ManageSecurityState?,
|
val manageSecurityState: ManageSecurityState?,
|
||||||
val resetCardAllowed: Boolean,
|
val resetCardAllowed: Boolean,
|
||||||
val resetButtonEnabled: Boolean,
|
val resetButtonEnabled: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,20 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.common.routing.utils.popTo
|
import com.tangem.common.routing.utils.popTo
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
||||||
import com.tangem.domain.card.ResetCardUseCase
|
import com.tangem.domain.card.ResetCardUseCase
|
||||||
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
|
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||||
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
|
import com.tangem.domain.wallets.legacy.asLockable
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
|
||||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||||
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||||
import com.tangem.tap.features.details.redux.CardSettingsState
|
import com.tangem.tap.features.details.redux.CardSettingsState
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
|
import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
|
||||||
|
|
@ -25,6 +32,7 @@ import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
|
import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class ResetCardViewModel @Inject constructor(
|
internal class ResetCardViewModel @Inject constructor(
|
||||||
private val resetCardFeatureToggles: ResetCardFeatureToggles,
|
private val resetCardFeatureToggles: ResetCardFeatureToggles,
|
||||||
|
|
@ -32,14 +40,17 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
private val resetCardUseCase: ResetCardUseCase,
|
private val resetCardUseCase: ResetCardUseCase,
|
||||||
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
||||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||||
|
private val userWalletsListManager: UserWalletsListManager,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val currentUserWallet = getSelectedWalletSyncUseCase().getOrNull()
|
private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse
|
||||||
?: error("Selected user wallet can't be null")
|
?: error("ScanResponse can't be null")
|
||||||
|
|
||||||
|
private val currentUserWalletId = createUserWalletId(firstCardScanResponse)
|
||||||
|
|
||||||
// TODO: move logic to separate domain entity
|
// TODO: move logic to separate domain entity
|
||||||
private val backupCardsCount = (currentUserWallet.getCardsCount() ?: 0) - 1
|
private var resetBackupCardCount = 0
|
||||||
private var resetCardsCount = 0
|
|
||||||
|
|
||||||
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
|
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
|
||||||
val descriptionText = state?.cardInfo
|
val descriptionText = state?.cardInfo
|
||||||
|
|
@ -84,7 +95,7 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
CardSettingsDialog.InterruptedResetDialog -> {
|
CardSettingsDialog.InterruptedResetDialog -> {
|
||||||
ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
|
ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
|
||||||
onConfirmClick = ::onContinueResetClick,
|
onConfirmClick = ::onContinueResetClick,
|
||||||
onDismiss = ::dismissAndFinishFullReset,
|
onDismiss = ::onInterruptedResetDialogDismiss,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CardSettingsDialog.CompletedResetDialog -> {
|
CardSettingsDialog.CompletedResetDialog -> {
|
||||||
|
|
@ -107,9 +118,9 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun makeFullReset() {
|
private fun makeFullReset() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
resetCardUseCase(card = currentUserWallet.scanResponse.card).onRight {
|
resetCardUseCase(card = firstCardScanResponse.card).onRight {
|
||||||
deleteSavedAccessCodesUseCase(currentUserWallet.cardId)
|
deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId)
|
||||||
deleteWalletUseCase(currentUserWallet.walletId)
|
deleteWalletUseCase(currentUserWalletId)
|
||||||
|
|
||||||
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||||
if (newSelectedWallet != null) {
|
if (newSelectedWallet != null) {
|
||||||
|
|
@ -128,12 +139,12 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
resetCardUseCase(
|
resetCardUseCase(
|
||||||
cardNumber = resetCardsCount + 1,
|
cardNumber = resetBackupCardCount + 1,
|
||||||
card = currentUserWallet.scanResponse.card,
|
card = firstCardScanResponse.card,
|
||||||
userWalletId = currentUserWallet.walletId,
|
userWalletId = currentUserWalletId,
|
||||||
)
|
)
|
||||||
.onRight {
|
.onRight {
|
||||||
resetCardsCount++
|
resetBackupCardCount++
|
||||||
|
|
||||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||||
|
|
||||||
|
|
@ -149,10 +160,23 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
showDialog(CardSettingsDialog.InterruptedResetDialog)
|
showDialog(CardSettingsDialog.InterruptedResetDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onInterruptedResetDialogDismiss() {
|
||||||
|
analyticsEventHandler.send(Settings.CardSettings.FactoryResetCanceled(cardsCount = resetBackupCardCount + 1))
|
||||||
|
|
||||||
|
dismissAndFinishFullReset()
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkRemainingBackupCards() {
|
private fun checkRemainingBackupCards() {
|
||||||
|
val backupCardsCount = firstCardScanResponse.getBackupCardsCount()
|
||||||
|
|
||||||
when {
|
when {
|
||||||
backupCardsCount > resetCardsCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
|
backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
|
||||||
backupCardsCount == resetCardsCount -> showDialog(CardSettingsDialog.CompletedResetDialog)
|
backupCardsCount == resetBackupCardCount -> {
|
||||||
|
analyticsEventHandler.send(
|
||||||
|
event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1),
|
||||||
|
)
|
||||||
|
showDialog(CardSettingsDialog.CompletedResetDialog)
|
||||||
|
}
|
||||||
else -> finishFullReset()
|
else -> finishFullReset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,12 +188,17 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finishFullReset() {
|
private fun finishFullReset() {
|
||||||
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
val newSelectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||||
|
|
||||||
if (newSelectedWallet != null) {
|
if (newSelectedWallet != null) {
|
||||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||||
} else {
|
} else {
|
||||||
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
|
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess
|
||||||
|
if (isLocked && userWalletsListManager.hasUserWallets) {
|
||||||
|
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
|
||||||
|
} else {
|
||||||
|
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,4 +209,21 @@ internal class ResetCardViewModel @Inject constructor(
|
||||||
private fun dismissDialog() {
|
private fun dismissDialog() {
|
||||||
store.dispatch(ResetToFactory.DismissDialog)
|
store.dispatch(ResetToFactory.DismissDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ScanResponse.getBackupCardsCount(): Int {
|
||||||
|
if (!cardTypesResolver.isMultiwalletAllowed()) return 0
|
||||||
|
|
||||||
|
return when (val status = card.backupStatus) {
|
||||||
|
is CardDTO.BackupStatus.Active -> status.cardCount
|
||||||
|
is CardDTO.BackupStatus.CardLinked,
|
||||||
|
is CardDTO.BackupStatus.NoBackup,
|
||||||
|
null,
|
||||||
|
-> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId {
|
||||||
|
return UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||||
|
?: error("UserWalletId can't be null")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ private const val HIDE_PROGRESS_DELAY = 400L
|
||||||
object HomeMiddleware {
|
object HomeMiddleware {
|
||||||
val handler = homeMiddleware
|
val handler = homeMiddleware
|
||||||
|
|
||||||
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/"
|
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.core.ui.components.*
|
import com.tangem.core.ui.components.*
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ sealed class AnalyticsParam {
|
||||||
data object Send : ScreensSources("Send")
|
data object Send : ScreensSources("Send")
|
||||||
data object Intro : ScreensSources("Introduction")
|
data object Intro : ScreensSources("Introduction")
|
||||||
data object MyWallets : ScreensSources("My Wallets")
|
data object MyWallets : ScreensSources("My Wallets")
|
||||||
|
data object Token : ScreensSources("Token")
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class TxSentFrom(val value: String) {
|
sealed class TxSentFrom(val value: String) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ data class ExchangeProvider(
|
||||||
|
|
||||||
@Json(name = "privacyPolicy")
|
@Json(name = "privacyPolicy")
|
||||||
val privacyPolicy: String?,
|
val privacyPolicy: String?,
|
||||||
|
|
||||||
|
@Json(name = "recommended")
|
||||||
|
val isRecommended: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ExchangeProviderType {
|
enum class ExchangeProviderType {
|
||||||
|
|
@ -31,4 +34,7 @@ enum class ExchangeProviderType {
|
||||||
|
|
||||||
@Json(name = "cex")
|
@Json(name = "cex")
|
||||||
CEX,
|
CEX,
|
||||||
|
|
||||||
|
@Json(name = "dex-bridge")
|
||||||
|
DEX_BRIDGE,
|
||||||
}
|
}
|
||||||
|
|
@ -23,34 +23,43 @@ data class ExchangeStatusResponse(
|
||||||
enum class ExchangeStatus {
|
enum class ExchangeStatus {
|
||||||
|
|
||||||
@Json(name = "new")
|
@Json(name = "new")
|
||||||
NEW,
|
New,
|
||||||
|
|
||||||
@Json(name = "waiting")
|
@Json(name = "waiting")
|
||||||
WAITING,
|
Waiting,
|
||||||
|
|
||||||
@Json(name = "confirming")
|
@Json(name = "confirming")
|
||||||
CONFIRMING,
|
Confirming,
|
||||||
|
|
||||||
@Json(name = "exchanging")
|
@Json(name = "exchanging")
|
||||||
EXCHANGING,
|
Exchanging,
|
||||||
|
|
||||||
@Json(name = "sending")
|
@Json(name = "sending")
|
||||||
SENDING,
|
Sending,
|
||||||
|
|
||||||
@Json(name = "finished")
|
@Json(name = "finished")
|
||||||
FINISHED,
|
Finished,
|
||||||
|
|
||||||
@Json(name = "failed")
|
@Json(name = "failed")
|
||||||
FAILED,
|
Failed,
|
||||||
|
|
||||||
@Json(name = "refunded")
|
@Json(name = "refunded")
|
||||||
REFUNDED,
|
Refunded,
|
||||||
|
|
||||||
@Json(name = "verifying")
|
@Json(name = "verifying")
|
||||||
VERIFYING,
|
Verifying,
|
||||||
|
|
||||||
@Json(name = "expired")
|
@Json(name = "expired")
|
||||||
CANCELLED,
|
Cancelled,
|
||||||
|
|
||||||
|
@Json(name = "waiting-tx-hash")
|
||||||
|
WaitingTxHash,
|
||||||
|
|
||||||
|
@Json(name = "tx-failed")
|
||||||
|
TxFailed,
|
||||||
|
|
||||||
|
@Json(name = "unknown")
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ExchangeStatusError(
|
data class ExchangeStatusError(
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,6 @@ object PreferencesKeys {
|
||||||
|
|
||||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||||
|
|
||||||
val IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
|
|
||||||
booleanPreferencesKey(name = "isWalletSwapPromoChangellyShown")
|
|
||||||
}
|
|
||||||
|
|
||||||
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
|
|
||||||
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
|
|
||||||
}
|
|
||||||
|
|
||||||
val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy {
|
val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy {
|
||||||
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
|
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +85,14 @@ object PreferencesKeys {
|
||||||
|
|
||||||
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
|
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
|
||||||
|
|
||||||
|
val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy {
|
||||||
|
booleanPreferencesKey(name = "isWalletSwapPromoOkxShown")
|
||||||
|
}
|
||||||
|
|
||||||
|
val IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY by lazy {
|
||||||
|
booleanPreferencesKey(name = "isTokenSwapPromoOkxShown")
|
||||||
|
}
|
||||||
|
|
||||||
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
|
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
|
||||||
|
|
||||||
// region Permission
|
// region Permission
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@
|
||||||
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
|
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
|
||||||
<string name="express_exchange_status_title">Статус обмена</string>
|
<string name="express_exchange_status_title">Статус обмена</string>
|
||||||
<string name="express_exchange_status_verifying">Требуется верификация</string>
|
<string name="express_exchange_status_verifying">Требуется верификация</string>
|
||||||
|
<string name="express_exchange_status_waiting_tx_hash">Ожидание хеша транзакции</string>
|
||||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
||||||
<string name="express_floating_rate">Плавающая ставка</string>
|
<string name="express_floating_rate">Плавающая ставка</string>
|
||||||
|
|
@ -248,6 +249,7 @@
|
||||||
<string name="express_provider_min_amount">Доступно с %s</string>
|
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||||
|
<string name="express_provider_recommended">Рекомендовано</string>
|
||||||
<string name="express_terms_of_use">Условиями использования</string>
|
<string name="express_terms_of_use">Условиями использования</string>
|
||||||
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
|
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
|
||||||
<string name="express_transaction_id">ID: %s</string>
|
<string name="express_transaction_id">ID: %s</string>
|
||||||
|
|
@ -584,6 +586,8 @@
|
||||||
<string name="story_meet_title">Встречайте Tangem</string>
|
<string name="story_meet_title">Встречайте Tangem</string>
|
||||||
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
|
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
|
||||||
<string name="story_web3_title">Поддержка Web 3.0</string>
|
<string name="story_web3_title">Поддержка Web 3.0</string>
|
||||||
|
<string name="swap_promo_text">Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке.</string>
|
||||||
|
<string name="swap_promo_title">Новый провайдер обмена!</string>
|
||||||
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
|
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
|
||||||
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
|
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
|
||||||
<string name="swapping_alert_title">Комиссии</string>
|
<string name="swapping_alert_title">Комиссии</string>
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,7 @@
|
||||||
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
|
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
|
||||||
<string name="express_exchange_status_title">Exchange status</string>
|
<string name="express_exchange_status_title">Exchange status</string>
|
||||||
<string name="express_exchange_status_verifying">Verification required</string>
|
<string name="express_exchange_status_verifying">Verification required</string>
|
||||||
|
<string name="express_exchange_status_waiting_tx_hash">Awaiting transaction hash</string>
|
||||||
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
||||||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||||
<string name="express_floating_rate">Floating rate</string>
|
<string name="express_floating_rate">Floating rate</string>
|
||||||
|
|
@ -251,6 +252,7 @@
|
||||||
<string name="express_provider_min_amount">Available from %s</string>
|
<string name="express_provider_min_amount">Available from %s</string>
|
||||||
<string name="express_provider_not_available">Unavailable for this pair</string>
|
<string name="express_provider_not_available">Unavailable for this pair</string>
|
||||||
<string name="express_provider_permission_needed">Permission Required</string>
|
<string name="express_provider_permission_needed">Permission Required</string>
|
||||||
|
<string name="express_provider_recommended">Recommended</string>
|
||||||
<string name="express_terms_of_use">Terms of Use</string>
|
<string name="express_terms_of_use">Terms of Use</string>
|
||||||
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
|
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
|
||||||
<string name="express_transaction_id">ID: %s</string>
|
<string name="express_transaction_id">ID: %s</string>
|
||||||
|
|
@ -610,6 +612,8 @@
|
||||||
<string name="story_meet_title">Meet Tangem</string>
|
<string name="story_meet_title">Meet Tangem</string>
|
||||||
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
|
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
|
||||||
<string name="story_web3_title">Web 3.0 Compatible</string>
|
<string name="story_web3_title">Web 3.0 Compatible</string>
|
||||||
|
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
|
||||||
|
<string name="swap_promo_title">New Swap Provider Available!</string>
|
||||||
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
|
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
|
||||||
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
|
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
|
||||||
<string name="swapping_alert_title">Fees</string>
|
<string name="swapping_alert_title">Fees</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,163 @@
|
||||||
|
package com.tangem.core.ui.components.notifications
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||||
|
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
|
||||||
|
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||||
|
import com.tangem.core.ui.res.TangemColorPalette.Dark6
|
||||||
|
import com.tangem.core.ui.res.TangemColorPalette.Light4
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
|
||||||
|
private val OxkPromoColor = Color(0xFFBCFF2F)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||||
|
.background(Dark6),
|
||||||
|
) {
|
||||||
|
Content(config = config)
|
||||||
|
Button(config = config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Content(config: NotificationConfig) {
|
||||||
|
Row {
|
||||||
|
Icon(
|
||||||
|
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors.icon.constant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = TangemTheme.dimens.spacing12)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(TangemTheme.dimens.spacing12),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = config.title.resolveReference(),
|
||||||
|
style = TangemTheme.typography.button,
|
||||||
|
color = OxkPromoColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = config.subtitle.resolveReference(),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.constantWhite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
config.onCloseClick?.let {
|
||||||
|
Icon(
|
||||||
|
painter = rememberVectorPainter(
|
||||||
|
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors.icon.constant,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(
|
||||||
|
top = TangemTheme.dimens.spacing8,
|
||||||
|
end = TangemTheme.dimens.spacing8,
|
||||||
|
)
|
||||||
|
.size(TangemTheme.dimens.size20)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
|
||||||
|
onClick = it,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Button(config: NotificationConfig) {
|
||||||
|
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||||
|
|
||||||
|
button?.let {
|
||||||
|
val isDarkMode = LocalIsInDarkTheme.current
|
||||||
|
TangemButton(
|
||||||
|
text = button.text.resolveReference(),
|
||||||
|
icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24),
|
||||||
|
onClick = button.onClick,
|
||||||
|
colors = TangemButtonColors(
|
||||||
|
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
|
||||||
|
contentColor = Dark6,
|
||||||
|
disabledBackgroundColor = TangemTheme.colors.button.disabled,
|
||||||
|
disabledContentColor = TangemTheme.colors.text.disabled,
|
||||||
|
),
|
||||||
|
enabled = true,
|
||||||
|
showProgress = false,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
start = TangemTheme.dimens.spacing12,
|
||||||
|
end = TangemTheme.dimens.spacing12,
|
||||||
|
bottom = TangemTheme.dimens.spacing12,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Preview
|
||||||
|
@Preview(widthDp = 360)
|
||||||
|
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun OkxPromoNotification_Preview(
|
||||||
|
@PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig,
|
||||||
|
) {
|
||||||
|
TangemThemePreview {
|
||||||
|
OkxPromoNotification(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider<NotificationConfig> {
|
||||||
|
override val values: Sequence<NotificationConfig>
|
||||||
|
get() = sequenceOf(
|
||||||
|
NotificationConfig(
|
||||||
|
title = resourceReference(R.string.swap_promo_title),
|
||||||
|
subtitle = resourceReference(R.string.swap_promo_text),
|
||||||
|
iconResId = R.drawable.img_okx_dex_logo,
|
||||||
|
onCloseClick = {},
|
||||||
|
),
|
||||||
|
NotificationConfig(
|
||||||
|
title = resourceReference(R.string.swap_promo_title),
|
||||||
|
subtitle = resourceReference(R.string.swap_promo_text),
|
||||||
|
iconResId = R.drawable.img_okx_dex_logo,
|
||||||
|
onCloseClick = {},
|
||||||
|
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||||
|
text = resourceReference(R.string.token_swap_promotion_button),
|
||||||
|
onClick = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
@ -59,7 +59,7 @@ fun getActiveIconRes(blockchainId: String): Int {
|
||||||
"pls", "pls/test" -> R.drawable.img_pls_22
|
"pls", "pls/test" -> R.drawable.img_pls_22
|
||||||
"zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22
|
"zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22
|
||||||
"moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22
|
"moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22
|
||||||
"manta", "manta/test" -> R.drawable.img_manta_22
|
"manta-pacific", "manta/test" -> R.drawable.img_manta_22
|
||||||
"polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22
|
"polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22
|
||||||
"moonriver", "moonriver/test" -> R.drawable.img_moonriver_22
|
"moonriver", "moonriver/test" -> R.drawable.img_moonriver_22
|
||||||
"mantle", "mantle/test" -> R.drawable.img_mantle_22
|
"mantle", "mantle/test" -> R.drawable.img_mantle_22
|
||||||
|
|
@ -269,7 +269,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
|
||||||
"pls", "pls/test" -> R.drawable.ic_pls_22
|
"pls", "pls/test" -> R.drawable.ic_pls_22
|
||||||
"zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22
|
"zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22
|
||||||
"moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22
|
"moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22
|
||||||
"manta", "manta/test" -> R.drawable.ic_manta_22
|
"manta-pacific", "manta/test" -> R.drawable.ic_manta_22
|
||||||
"polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22
|
"polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22
|
||||||
"moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22
|
"moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22
|
||||||
"mantle", "mantle/test" -> R.drawable.ic_mantle_22
|
"mantle", "mantle/test" -> R.drawable.ic_mantle_22
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ class TangemColors internal constructor(
|
||||||
warning: Color,
|
warning: Color,
|
||||||
attention: Color,
|
attention: Color,
|
||||||
accent: Color = TangemColorPalette.Azure,
|
accent: Color = TangemColorPalette.Azure,
|
||||||
|
constant: Color = TangemColorPalette.White,
|
||||||
) {
|
) {
|
||||||
var primary1 by mutableStateOf(primary1)
|
var primary1 by mutableStateOf(primary1)
|
||||||
private set
|
private set
|
||||||
|
|
@ -100,6 +101,7 @@ class TangemColors internal constructor(
|
||||||
private set
|
private set
|
||||||
var attention by mutableStateOf(attention)
|
var attention by mutableStateOf(attention)
|
||||||
private set
|
private set
|
||||||
|
var constant by mutableStateOf(constant)
|
||||||
|
|
||||||
fun update(other: Icon) {
|
fun update(other: Icon) {
|
||||||
primary1 = other.primary1
|
primary1 = other.primary1
|
||||||
|
|
@ -110,6 +112,7 @@ class TangemColors internal constructor(
|
||||||
accent = other.accent
|
accent = other.accent
|
||||||
warning = other.warning
|
warning = other.warning
|
||||||
attention = other.attention
|
attention = other.attention
|
||||||
|
constant = other.constant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.core.ui.screen
|
package com.tangem.core.ui.screen
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
|
@ -22,6 +23,18 @@ abstract class ComposeFragment : Fragment(), ComposeScreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We need to manually dispatch configuration changes to the Compose view.
|
||||||
|
*
|
||||||
|
|
||||||
|
* `android:configChanges="uiMode"` is set in the manifest.
|
||||||
|
* */
|
||||||
|
view?.dispatchConfigurationChanged(newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inflates transitions for the fragment. Override this method to customize
|
* Inflates transitions for the fragment. Override this method to customize
|
||||||
* enter and exit transitions for the fragment.
|
* enter and exit transitions for the fragment.
|
||||||
|
|
|
||||||
29
core/ui/src/main/res/drawable/img_okx_dex_logo.xml
Normal file
29
core/ui/src/main/res/drawable/img_okx_dex_logo.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="1232.1" android:viewportWidth="2516" android:width="49.00901dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m740.2,0L23.5,0c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.6c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h716.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11L756.9,15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6ZM506.9,452.3c0,4.1 -1.8,8.1 -4.9,11 -3.1,2.9 -7.4,4.6 -11.8,4.6h-216.7c-4.4,0 -8.7,-1.6 -11.8,-4.6 -3.1,-2.9 -4.9,-6.9 -4.9,-11v-202.8c0,-4.1 1.8,-8.1 4.9,-11 3.1,-2.9 7.4,-4.6 11.8,-4.6h216.7c4.4,0 8.7,1.6 11.8,4.6 3.1,2.9 4.9,6.9 4.9,11v202.8Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m2240.6,234h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6h0Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1990.7,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m2490.8,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1990.7,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m2490.8,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1615.3,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1615.3,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1381.9,249.4c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-233.4V15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-216.7c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.3c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h216.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-218.2h233.4c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-202.8Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m1675.2,885c33.8,0 64.5,6.8 92,20.4 27.5,13.6 49.2,33.4 65.2,59.5 16,26.1 24,57.2 24,93.2s-8,67.2 -24,93.4c-16,26.3 -37.6,46.2 -65,60 -27.4,13.8 -58.1,20.6 -92.2,20.6h-125.2v-347.1h125.2ZM1666.2,1163.8c34.5,0 60.5,-9.2 78.3,-27.5 17.7,-18.3 26.6,-44.4 26.6,-78.3s-8.9,-59.4 -26.8,-77.5c-17.9,-18.2 -43.9,-27.3 -78,-27.3h-35.1v210.6h35.1Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m2125.7,1092.2h-135.6v69.7h157v70.2h-238.1v-347.1h232.4v69.7h-151.3v68.3h135.6v69.2Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="m2509.8,885l-121.4,168.4 127.6,178.8h-100.1l-77.3,-112.9 -78.7,112.9h-92.9l127.6,-176.9 -121.4,-170.3h99.1l71.6,104.3 72.6,-104.3h93.4Z"/>
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M0,1033.9h1381.6v49h-1381.6z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
package com.tangem.data.card.sdk
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lifecycle observer for creating Card SDK instance
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
interface CardSdkLifecycleObserver {
|
|
||||||
|
|
||||||
/** Callback of creating activity [context] */
|
|
||||||
fun onCreate(context: Context)
|
|
||||||
|
|
||||||
/** Callback of destroying activity [context] */
|
|
||||||
fun onDestroy(context: Context)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.tangem.data.card.sdk
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle observer for creating Card SDK instance
|
||||||
|
*
|
||||||
|
[REDACTED_AUTHOR]
|
||||||
|
*/
|
||||||
|
interface CardSdkOwner {
|
||||||
|
|
||||||
|
fun register(activity: FragmentActivity)
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import com.tangem.TangemSdk
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
internal interface CardSdkProvider {
|
interface CardSdkProvider {
|
||||||
|
|
||||||
/** CardSDK instance */
|
/** CardSDK instance */
|
||||||
val sdk: TangemSdk
|
val sdk: TangemSdk
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
package com.tangem.data.card.sdk
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import com.tangem.TangemSdk
|
|
||||||
import com.tangem.common.CardFilter
|
|
||||||
import com.tangem.common.card.FirmwareVersion
|
|
||||||
import com.tangem.common.core.Config
|
|
||||||
import com.tangem.sdk.extensions.initWithBiometrics
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of CardSDK instance provider
|
|
||||||
*
|
|
||||||
[REDACTED_AUTHOR]
|
|
||||||
*/
|
|
||||||
@Singleton
|
|
||||||
internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver {
|
|
||||||
|
|
||||||
override val sdk: TangemSdk
|
|
||||||
get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" }
|
|
||||||
|
|
||||||
private var _sdk: TangemSdk? = null
|
|
||||||
|
|
||||||
override fun onCreate(context: Context) {
|
|
||||||
_sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy(context: Context) {
|
|
||||||
// Commented out to prevent crash on getting sdk when it's null.
|
|
||||||
// FIXME: We still should find the real cause and fix it properly.
|
|
||||||
// idea: pass everywhere DefaultCardSdkProvider instead sdk to reach lazy access to sdk property
|
|
||||||
// _sdk = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
|
|
||||||
val config = Config(
|
|
||||||
linkedTerminal = true,
|
|
||||||
allowUntrustedCards = true,
|
|
||||||
filter = CardFilter(
|
|
||||||
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
|
|
||||||
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
|
|
||||||
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
|
|
||||||
items = setOf("0027", "0030", "0031", "0035"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -32,8 +32,20 @@ internal class DefaultPromoRepository(
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getOkxPromoBanner(): PromoBanner? {
|
||||||
|
// TODO disabled for 5.12, enable for 5.12.1
|
||||||
|
return null
|
||||||
|
// return runCatching(dispatchers.io) {
|
||||||
|
// promoResponseConverter.convert(
|
||||||
|
// tangemApi.getPromotionInfo(OKX)
|
||||||
|
// .getOrThrow(),
|
||||||
|
// )
|
||||||
|
// }.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private const val CHANGELLY_NAME = "changelly"
|
private const val CHANGELLY_NAME = "changelly"
|
||||||
private const val TRAVALA = "travala"
|
private const val TRAVALA = "travala"
|
||||||
|
// private const val OKX = "okx"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.tangem.data.settings
|
package com.tangem.data.settings
|
||||||
|
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY
|
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY
|
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY
|
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY
|
||||||
import com.tangem.datasource.local.preferences.utils.get
|
import com.tangem.datasource.local.preferences.utils.get
|
||||||
import com.tangem.datasource.local.preferences.utils.store
|
import com.tangem.datasource.local.preferences.utils.store
|
||||||
|
|
@ -16,23 +16,23 @@ class DefaultPromoSettingsRepository(
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
private val appPreferencesStore: AppPreferencesStore,
|
||||||
) : PromoSettingsRepository {
|
) : PromoSettingsRepository {
|
||||||
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
|
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
|
||||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
|
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setNeverToShowWalletSwapPromo() {
|
override suspend fun setNeverToShowWalletSwapPromo() {
|
||||||
appPreferencesStore.store(
|
appPreferencesStore.store(
|
||||||
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
|
||||||
value = false,
|
value = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setNeverToShowTokenSwapPromo() {
|
override suspend fun setNeverToShowTokenSwapPromo() {
|
||||||
appPreferencesStore.store(
|
appPreferencesStore.store(
|
||||||
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
|
||||||
value = false,
|
value = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,8 +87,11 @@ internal object TokensDataModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideDefaultMarketCoinsRepository(expressAssetsStore: ExpressAssetsStore): MarketCryptoCurrencyRepository {
|
fun provideDefaultMarketCoinsRepository(
|
||||||
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore)
|
expressAssetsStore: ExpressAssetsStore,
|
||||||
|
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||||
|
): MarketCryptoCurrencyRepository {
|
||||||
|
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
||||||
|
|
@ -314,26 +314,28 @@ internal class DefaultCurrenciesRepository(
|
||||||
networkId: Network.ID,
|
networkId: Network.ID,
|
||||||
derivationPath: Network.DerivationPath,
|
derivationPath: Network.DerivationPath,
|
||||||
): CryptoCurrency.Coin {
|
): CryptoCurrency.Coin {
|
||||||
val userWallet = getUserWallet(userWalletId)
|
return withContext(dispatchers.io) {
|
||||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
val userWallet = getUserWallet(userWalletId)
|
||||||
|
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||||
|
|
||||||
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
|
||||||
|
|
||||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||||
|
}
|
||||||
|
val blockchain = Blockchain.fromId(networkId.value)
|
||||||
|
val blockchainNetworkId = blockchain.toNetworkId()
|
||||||
|
val coinId = blockchain.toCoinId()
|
||||||
|
|
||||||
|
val storedCoin = storedTokens.tokens
|
||||||
|
.find {
|
||||||
|
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
||||||
|
} ?: error("Coin in this network $networkId not found")
|
||||||
|
|
||||||
|
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
||||||
|
|
||||||
|
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
||||||
}
|
}
|
||||||
val blockchain = Blockchain.fromId(networkId.value)
|
|
||||||
val blockchainNetworkId = blockchain.toNetworkId()
|
|
||||||
val coinId = blockchain.toCoinId()
|
|
||||||
|
|
||||||
val storedCoin = storedTokens.tokens
|
|
||||||
.find {
|
|
||||||
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
|
|
||||||
} ?: error("Coin in this network $networkId not found")
|
|
||||||
|
|
||||||
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
|
|
||||||
|
|
||||||
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
|
||||||
|
|
@ -395,29 +397,31 @@ internal class DefaultCurrenciesRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
|
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
|
||||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
return withContext(dispatchers.io) {
|
||||||
return when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||||
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
||||||
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
||||||
is FeePaidSdkCurrency.Token -> {
|
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
||||||
val balance = walletManagersFacade.tokenBalance(
|
is FeePaidSdkCurrency.Token -> {
|
||||||
userWalletId = userWalletId,
|
val balance = walletManagersFacade.tokenBalance(
|
||||||
network = currency.network,
|
userWalletId = userWalletId,
|
||||||
name = feePaidCurrency.token.name,
|
network = currency.network,
|
||||||
symbol = feePaidCurrency.token.symbol,
|
name = feePaidCurrency.token.name,
|
||||||
contractAddress = feePaidCurrency.token.contractAddress,
|
symbol = feePaidCurrency.token.symbol,
|
||||||
decimals = feePaidCurrency.token.decimals,
|
contractAddress = feePaidCurrency.token.contractAddress,
|
||||||
id = feePaidCurrency.token.id,
|
decimals = feePaidCurrency.token.decimals,
|
||||||
)
|
id = feePaidCurrency.token.id,
|
||||||
FeePaidCurrency.Token(
|
)
|
||||||
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
FeePaidCurrency.Token(
|
||||||
name = feePaidCurrency.token.name,
|
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
||||||
symbol = feePaidCurrency.token.symbol,
|
name = feePaidCurrency.token.name,
|
||||||
contractAddress = feePaidCurrency.token.contractAddress,
|
symbol = feePaidCurrency.token.symbol,
|
||||||
balance = balance,
|
contractAddress = feePaidCurrency.token.contractAddress,
|
||||||
)
|
balance = balance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
|
||||||
}
|
}
|
||||||
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import com.tangem.datasource.local.token.ExpressAssetsStore
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DefaultMarketCryptoCurrencyRepository(
|
class DefaultMarketCryptoCurrencyRepository(
|
||||||
private val expressAssetsStore: ExpressAssetsStore,
|
private val expressAssetsStore: ExpressAssetsStore,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : MarketCryptoCurrencyRepository {
|
) : MarketCryptoCurrencyRepository {
|
||||||
|
|
||||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||||
|
|
@ -15,11 +18,16 @@ class DefaultMarketCryptoCurrencyRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||||
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
return withContext(dispatchers.io) {
|
||||||
|
val contractAddress =
|
||||||
|
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
|
||||||
|
|
||||||
return expressAssetsStore.getSyncOrNull(userWalletId)?.find {
|
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
|
||||||
it.network == cryptoCurrency.network.backendId &&
|
it.network == cryptoCurrency.network.backendId &&
|
||||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||||
}?.exchangeAvailable ?: false
|
}
|
||||||
|
|
||||||
|
asset?.exchangeAvailable ?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +51,19 @@ internal class DefaultQuotesRepository(
|
||||||
.flowOn(dispatchers.io)
|
.flowOn(dispatchers.io)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {
|
||||||
|
withContext(dispatchers.io) {
|
||||||
|
val selectedAppCurrency = requireNotNull(
|
||||||
|
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
|
||||||
|
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
|
||||||
|
),
|
||||||
|
lazyMessage = { "Unable to get selected application currency to update quotes" },
|
||||||
|
)
|
||||||
|
|
||||||
|
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
|
||||||
return withContext(dispatchers.io) {
|
return withContext(dispatchers.io) {
|
||||||
val selectedAppCurrency = requireNotNull(
|
val selectedAppCurrency = requireNotNull(
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,19 @@ class DefaultTxHistoryRepository(
|
||||||
private val sdkPageConverter by lazy { SdkPageConverter() }
|
private val sdkPageConverter by lazy { SdkPageConverter() }
|
||||||
|
|
||||||
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
|
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
|
||||||
val userWallet = getUserWallet(userWalletId)
|
return withContext(dispatchers.io) {
|
||||||
val state = walletManagersFacade.getTxHistoryState(
|
val userWallet = getUserWallet(userWalletId)
|
||||||
userWalletId = userWallet.walletId,
|
val state = walletManagersFacade.getTxHistoryState(
|
||||||
currency = currency,
|
userWalletId = userWallet.walletId,
|
||||||
)
|
currency = currency,
|
||||||
return when (state) {
|
)
|
||||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
|
||||||
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
when (state) {
|
||||||
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||||
|
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||||
|
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package com.tangem.domain.card.models
|
package com.tangem.domain.card.models
|
||||||
|
|
||||||
sealed interface ResetCardError {
|
sealed class ResetCardError {
|
||||||
|
|
||||||
data object UserCanceled : ResetCardError
|
data object UserCanceled : ResetCardError()
|
||||||
|
|
||||||
data object AnotherSdkError : ResetCardError
|
data object SdkError : ResetCardError()
|
||||||
}
|
}
|
||||||
|
|
@ -5,11 +5,11 @@ import com.tangem.domain.core.utils.lceContent
|
||||||
import com.tangem.domain.core.utils.lceError
|
import com.tangem.domain.core.utils.lceError
|
||||||
import com.tangem.domain.core.utils.lceLoading
|
import com.tangem.domain.core.utils.lceLoading
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.ProducerScope
|
import kotlinx.coroutines.channels.ProducerScope
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.experimental.ExperimentalTypeInference
|
import kotlin.experimental.ExperimentalTypeInference
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -25,35 +25,43 @@ typealias LceFlow<E, C> = Flow<Lce<E, C>>
|
||||||
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
|
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
|
||||||
*
|
*
|
||||||
* @property raise The [LceRaise] instance that this class wraps.
|
* @property raise The [LceRaise] instance that this class wraps.
|
||||||
* @property scope The [ProducerScope] that this class operates within.
|
* @property producerScope The [ProducerScope] instance that this class wraps.
|
||||||
* @property ifLoading The function to call if a loading state is raised.
|
* @property ifLoading The function to call if a loading state is raised.
|
||||||
*/
|
*/
|
||||||
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||||
private val raise: LceRaise<E>,
|
private val raise: LceRaise<E>,
|
||||||
private val scope: ProducerScope<Lce<E, C>>,
|
private val producerScope: ProducerScope<Lce<E, C>>,
|
||||||
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
|
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
|
||||||
) : Raise<E>, CoroutineScope by scope {
|
) : Raise<E>, CoroutineScope by producerScope {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Raises an [Lce] instance within the [ProducerScope].
|
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
|
||||||
* It closes the [ProducerScope] after raise.
|
* All subsequent sends will be ignored.
|
||||||
*
|
*
|
||||||
* @param r The [Lce] instance to raise.
|
* This method blocks the coroutine until a error is handled by the receiver.
|
||||||
|
*
|
||||||
|
* If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r]
|
||||||
|
* without closing.
|
||||||
|
*
|
||||||
|
* @param r Error to raise.
|
||||||
*/
|
*/
|
||||||
override fun raise(r: E): Nothing {
|
override fun raise(r: E): Nothing {
|
||||||
scope.launch(NonCancellable) {
|
producerScope.trySendBlocking(r.lceError())
|
||||||
scope.send(r.lceError())
|
producerScope.close()
|
||||||
scope.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
raise.raise(r.lceError())
|
raise.raise(r.lceError())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a content value within the [ProducerScope].
|
* Sends a [content] value within the [ProducerScope].
|
||||||
|
*
|
||||||
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
|
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
|
||||||
* Otherwise, it wraps the content in a [Lce.Content] state.
|
* Otherwise, it wraps the content in a [Lce.Content] state.
|
||||||
*
|
*
|
||||||
|
* This method suspends until the [Lce] instance is handled by the receiver.
|
||||||
|
*
|
||||||
|
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
|
||||||
|
*
|
||||||
* @param content The content value to send.
|
* @param content The content value to send.
|
||||||
* @param isStillLoading A flag indicating whether the content is still loading.
|
* @param isStillLoading A flag indicating whether the content is still loading.
|
||||||
*/
|
*/
|
||||||
|
|
@ -65,11 +73,23 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||||
content.lceContent()
|
content.lceContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
scope.send(value)
|
send(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an [Lce] instance within the [ProducerScope].
|
||||||
|
*
|
||||||
|
* This method suspends until the [Lce] instance is handled by the receiver.
|
||||||
|
*
|
||||||
|
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
|
||||||
|
*
|
||||||
|
* @param value The [Lce] instance to send.
|
||||||
|
*/
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
suspend fun send(value: Lce<E, C>) {
|
suspend fun send(value: Lce<E, C>) {
|
||||||
scope.send(value)
|
if (producerScope.isClosedForSend) return
|
||||||
|
|
||||||
|
producerScope.send(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +114,7 @@ fun <E : Any, C : Any> lceFlow(
|
||||||
lce {
|
lce {
|
||||||
val scope = LceFlowScope(
|
val scope = LceFlowScope(
|
||||||
raise = this@lce,
|
raise = this@lce,
|
||||||
scope = this@channelFlow,
|
producerScope = this@channelFlow,
|
||||||
ifLoading = ifLoading,
|
ifLoading = ifLoading,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ interface CardTypesResolver {
|
||||||
|
|
||||||
fun isVoltInuWallet(): Boolean
|
fun isVoltInuWallet(): Boolean
|
||||||
|
|
||||||
|
fun isVividWallet(): Boolean
|
||||||
|
|
||||||
|
fun isPastelWallet(): Boolean
|
||||||
|
|
||||||
fun isWhiteWallet(): Boolean
|
fun isWhiteWallet(): Boolean
|
||||||
|
|
||||||
fun isWallet2(): Boolean
|
fun isWallet2(): Boolean
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@ internal class TangemCardTypesResolver(
|
||||||
|
|
||||||
override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID
|
override fun isVoltInuWallet(): Boolean = card.batchId == VOLT_INU_WALLET_BATCH_ID
|
||||||
|
|
||||||
|
override fun isVividWallet(): Boolean = card.batchId == VIVID_LEMON_WALLET_BATCH_ID ||
|
||||||
|
card.batchId == VIVID_AQUA_WALLET_BATCH_ID ||
|
||||||
|
card.batchId == VIVID_GRAPEFRUIT_WALLET_BATCH_ID
|
||||||
|
|
||||||
|
override fun isPastelWallet(): Boolean = card.batchId == PASTEL_PEACH_WALLET_BATCH_ID ||
|
||||||
|
card.batchId == PASTEL_GRASS_WALLET_BATCH_ID ||
|
||||||
|
card.batchId == PASTEL_AIR_WALLET_BATCH_ID
|
||||||
|
|
||||||
override fun isWhiteWallet(): Boolean {
|
override fun isWhiteWallet(): Boolean {
|
||||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||||
}
|
}
|
||||||
|
|
@ -190,5 +198,13 @@ internal class TangemCardTypesResolver(
|
||||||
const val COQ_WALLET_BATCH_ID = "AF28"
|
const val COQ_WALLET_BATCH_ID = "AF28"
|
||||||
const val COIN_METRICA_WALLET_BATCH_ID = "AF27"
|
const val COIN_METRICA_WALLET_BATCH_ID = "AF27"
|
||||||
const val VOLT_INU_WALLET_BATCH_ID = "AF35"
|
const val VOLT_INU_WALLET_BATCH_ID = "AF35"
|
||||||
|
// VIVID WALLETS
|
||||||
|
const val VIVID_LEMON_WALLET_BATCH_ID = "AF40"
|
||||||
|
const val VIVID_AQUA_WALLET_BATCH_ID = "AF41"
|
||||||
|
const val VIVID_GRAPEFRUIT_WALLET_BATCH_ID = "AF42"
|
||||||
|
// PASTEL WALLETS
|
||||||
|
const val PASTEL_PEACH_WALLET_BATCH_ID = "AF43"
|
||||||
|
const val PASTEL_AIR_WALLET_BATCH_ID = "AF44"
|
||||||
|
const val PASTEL_GRASS_WALLET_BATCH_ID = "AF45"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -87,11 +87,13 @@ class DefaultWalletManagersFacade(
|
||||||
Blockchain.fromId(it.id.value) to it.derivationPath.value
|
Blockchain.fromId(it.id.value) to it.derivationPath.value
|
||||||
}
|
}
|
||||||
|
|
||||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
withContext(dispatchers.io) {
|
||||||
val wallet = walletManager.wallet
|
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||||
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
val wallet = walletManager.wallet
|
||||||
|
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
||||||
|
|
||||||
blockchainToDerivationPath in blockchainsToDerivationPaths
|
blockchainToDerivationPath in blockchainsToDerivationPaths
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,18 +112,20 @@ class DefaultWalletManagersFacade(
|
||||||
network: Network,
|
network: Network,
|
||||||
networkTokens: List<CryptoCurrency.Token>,
|
networkTokens: List<CryptoCurrency.Token>,
|
||||||
) {
|
) {
|
||||||
val walletManager = walletManagersStore.getSyncOrNull(
|
withContext(dispatchers.io) {
|
||||||
userWalletId = userWalletId,
|
val walletManager = walletManagersStore.getSyncOrNull(
|
||||||
blockchain = Blockchain.fromId(network.id.value),
|
userWalletId = userWalletId,
|
||||||
derivationPath = network.derivationPath.value,
|
blockchain = Blockchain.fromId(network.id.value),
|
||||||
) ?: return
|
derivationPath = network.derivationPath.value,
|
||||||
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
) ?: return@withContext
|
||||||
|
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
||||||
|
|
||||||
tokensToRemove.forEach { token ->
|
tokensToRemove.forEach { token ->
|
||||||
walletManager.removeToken(token)
|
walletManager.removeToken(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
walletManagersStore.store(userWalletId, walletManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
walletManagersStore.store(userWalletId, walletManager)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updatePendingTransactions(
|
override suspend fun updatePendingTransactions(
|
||||||
|
|
@ -576,12 +580,16 @@ class DefaultWalletManagersFacade(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
currency: CryptoCurrency,
|
currency: CryptoCurrency,
|
||||||
): AssetRequirementsCondition? {
|
): AssetRequirementsCondition? {
|
||||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
return withContext(dispatchers.io) {
|
||||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||||
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null
|
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||||
|
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) {
|
||||||
|
return@withContext null
|
||||||
|
}
|
||||||
|
|
||||||
val condition = walletManager.requirementsCondition(currencyType) ?: return null
|
val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null
|
||||||
return requirementsConditionConverter.convert(condition)
|
requirementsConditionConverter.convert(condition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun associateAsset(
|
override suspend fun associateAsset(
|
||||||
|
|
@ -589,15 +597,18 @@ class DefaultWalletManagersFacade(
|
||||||
currency: CryptoCurrency,
|
currency: CryptoCurrency,
|
||||||
signer: CommonSigner,
|
signer: CommonSigner,
|
||||||
): SimpleResult {
|
): SimpleResult {
|
||||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
return withContext(dispatchers.io) {
|
||||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||||
|
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||||
|
|
||||||
if (walletManager !is AssetRequirementsManager) {
|
if (walletManager !is AssetRequirementsManager) {
|
||||||
return SimpleResult.Failure(
|
return@withContext SimpleResult.Failure(
|
||||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
walletManager.fulfillRequirements(currencyType, signer)
|
||||||
}
|
}
|
||||||
return walletManager.fulfillRequirements(currencyType, signer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean {
|
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,43 @@
|
||||||
package com.tangem.domain.tokens.models.analytics
|
package com.tangem.domain.tokens.models.analytics
|
||||||
|
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
|
|
||||||
sealed class TokenSwapPromoAnalyticsEvent(
|
sealed class TokenSwapPromoAnalyticsEvent(
|
||||||
event: String,
|
event: String,
|
||||||
params: Map<String, String> = mapOf(),
|
params: Map<String, String> = mapOf(),
|
||||||
) : AnalyticsEvent("Swap Promo", event, params, null) {
|
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
||||||
|
class NoticePromotionBanner(
|
||||||
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
|
source: AnalyticsParam.ScreensSources,
|
||||||
|
programName: ProgramName,
|
||||||
class Exchange(
|
|
||||||
token: String,
|
|
||||||
) : TokenSwapPromoAnalyticsEvent(
|
) : TokenSwapPromoAnalyticsEvent(
|
||||||
event = "Button - Exchange Now",
|
event = "Notice - Promotion Banner",
|
||||||
params = mapOf("Token" to token),
|
params = mapOf(
|
||||||
|
AnalyticsParam.SOURCE to source.value,
|
||||||
|
"Program Name" to programName.name,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class PromotionBannerClicked(
|
||||||
|
source: AnalyticsParam.ScreensSources,
|
||||||
|
programName: ProgramName,
|
||||||
|
action: BannerAction,
|
||||||
|
) : TokenSwapPromoAnalyticsEvent(
|
||||||
|
event = "Promo Banner Clicked",
|
||||||
|
params = mapOf(
|
||||||
|
AnalyticsParam.SOURCE to source.value,
|
||||||
|
"Program Name" to programName.name,
|
||||||
|
"Action" to action.action,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
sealed class BannerAction(val action: String) {
|
||||||
|
data object Clicked : BannerAction(action = "Clicked")
|
||||||
|
data object Closed : BannerAction(action = "Closed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ProgramName {
|
||||||
|
Travala,
|
||||||
|
OKX,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import arrow.core.raise.Raise
|
||||||
import arrow.core.raise.catch
|
import arrow.core.raise.catch
|
||||||
import arrow.core.raise.either
|
import arrow.core.raise.either
|
||||||
import arrow.core.toNonEmptyListOrNull
|
import arrow.core.toNonEmptyListOrNull
|
||||||
import com.tangem.domain.tokens.error.AddCurrencyError
|
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.Network
|
import com.tangem.domain.tokens.model.Network
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
|
|
@ -32,9 +31,9 @@ class AddCryptoCurrenciesUseCase(
|
||||||
*
|
*
|
||||||
* @param userWalletId The ID of the user's wallet.
|
* @param userWalletId The ID of the user's wallet.
|
||||||
* @param currency Cryptocurrency to add.
|
* @param currency Cryptocurrency to add.
|
||||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<AddCurrencyError, Unit> {
|
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Unit> {
|
||||||
return invoke(userWalletId, listOf(currency))
|
return invoke(userWalletId, listOf(currency))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,13 +46,13 @@ class AddCryptoCurrenciesUseCase(
|
||||||
* @param userWalletId The ID of the user's wallet.
|
* @param userWalletId The ID of the user's wallet.
|
||||||
* @param cryptoCurrency Token to add.
|
* @param cryptoCurrency Token to add.
|
||||||
* @param network Network where we add
|
* @param network Network where we add
|
||||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
cryptoCurrency: CryptoCurrency.Token,
|
cryptoCurrency: CryptoCurrency.Token,
|
||||||
network: Network,
|
network: Network,
|
||||||
): Either<AddCurrencyError, Unit> = either {
|
): Either<Throwable, Unit> = either {
|
||||||
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network)
|
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network)
|
||||||
invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
||||||
}
|
}
|
||||||
|
|
@ -66,14 +65,14 @@ class AddCryptoCurrenciesUseCase(
|
||||||
*
|
*
|
||||||
* @param userWalletId The ID of the user's wallet.
|
* @param userWalletId The ID of the user's wallet.
|
||||||
* @param currencies The list of cryptocurrencies to add.
|
* @param currencies The list of cryptocurrencies to add.
|
||||||
* @return Either an [AddCurrencyError] or [Unit] indicating the success of the operation.
|
* @return Either an [Throwable] or [Unit] indicating the success of the operation.
|
||||||
*/
|
*/
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
currencies: List<CryptoCurrency>,
|
currencies: List<CryptoCurrency>,
|
||||||
): Either<AddCurrencyError, Unit> = either {
|
): Either<Throwable, Unit> = either {
|
||||||
val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
|
val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
|
||||||
raise(AddCurrencyError.DataError(it))
|
raise(it)
|
||||||
}
|
}
|
||||||
val currenciesToAdd = currencies
|
val currenciesToAdd = currencies
|
||||||
.filterNot(existingCurrencies::contains)
|
.filterNot(existingCurrencies::contains)
|
||||||
|
|
@ -81,7 +80,7 @@ class AddCryptoCurrenciesUseCase(
|
||||||
?: return@either
|
?: return@either
|
||||||
|
|
||||||
catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) {
|
catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) {
|
||||||
raise(AddCurrencyError.DataError(it))
|
raise(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies)
|
refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies)
|
||||||
|
|
@ -91,7 +90,7 @@ class AddCryptoCurrenciesUseCase(
|
||||||
* Refreshes the network statuses for tokens that have corresponding coins in the
|
* Refreshes the network statuses for tokens that have corresponding coins in the
|
||||||
* [existingCurrencies] list.
|
* [existingCurrencies] list.
|
||||||
*/
|
*/
|
||||||
private suspend fun Raise<AddCurrencyError>.refreshUpdatedNetworks(
|
private suspend fun Raise<Throwable>.refreshUpdatedNetworks(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
currenciesToAdd: List<CryptoCurrency>,
|
currenciesToAdd: List<CryptoCurrency>,
|
||||||
existingCurrencies: List<CryptoCurrency>,
|
existingCurrencies: List<CryptoCurrency>,
|
||||||
|
|
@ -114,7 +113,7 @@ class AddCryptoCurrenciesUseCase(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
raise(AddCurrencyError.DataError(it))
|
raise(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ class GetCurrencyWarningsUseCase(
|
||||||
): Flow<CryptoCurrencyWarning?> {
|
): Flow<CryptoCurrencyWarning?> {
|
||||||
val currency = currencyStatus.currency
|
val currency = currencyStatus.currency
|
||||||
val cryptoStatuses = operations.getCurrenciesStatusesSync()
|
val cryptoStatuses = operations.getCurrenciesStatusesSync()
|
||||||
val promoBanner = promoRepository.getChangellyPromoBanner()
|
val promoBanner = promoRepository.getOkxPromoBanner()
|
||||||
return combine(
|
return combine(
|
||||||
showSwapPromoTokenUseCase().conflate(),
|
showSwapPromoTokenUseCase().conflate(),
|
||||||
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),
|
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.tangem.domain.tokens
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.getOrElse
|
||||||
|
import arrow.core.raise.catch
|
||||||
|
import arrow.core.raise.either
|
||||||
|
import com.tangem.domain.tokens.error.QuotesError
|
||||||
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
|
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
|
||||||
|
class RefreshMultiCurrencyWalletQuotesUseCase(
|
||||||
|
private val quotesRepository: QuotesRepository,
|
||||||
|
private val currenciesRepository: CurrenciesRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(userWalletId: UserWalletId): Either<QuotesError, Unit> {
|
||||||
|
return either {
|
||||||
|
val currencies = fetchCurrencies(userWalletId = userWalletId)
|
||||||
|
.getOrElse { raise(QuotesError.DataError(it)) }
|
||||||
|
|
||||||
|
coroutineScope {
|
||||||
|
val fetchQuotes = async {
|
||||||
|
fetchQuotes(
|
||||||
|
currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitAll(fetchQuotes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchCurrencies(userWalletId: UserWalletId): Either<Throwable, List<CryptoCurrency>> {
|
||||||
|
return either {
|
||||||
|
catch(
|
||||||
|
block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, false) },
|
||||||
|
catch = { raise(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {
|
||||||
|
catch(
|
||||||
|
block = { quotesRepository.fetchQuotes(currenciesIds) },
|
||||||
|
catch = { /* Ignore error */ },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package com.tangem.domain.tokens.error
|
|
||||||
|
|
||||||
sealed class AddCurrencyError {
|
|
||||||
|
|
||||||
data class DataError(val cause: Throwable) : AddCurrencyError()
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.tangem.domain.tokens.error
|
||||||
|
|
||||||
|
sealed class QuotesError {
|
||||||
|
data class DataError(val cause: Throwable) : QuotesError()
|
||||||
|
}
|
||||||
|
|
@ -7,4 +7,6 @@ interface PromoRepository {
|
||||||
suspend fun getChangellyPromoBanner(): PromoBanner?
|
suspend fun getChangellyPromoBanner(): PromoBanner?
|
||||||
|
|
||||||
suspend fun getTravalaPromoBanner(): PromoBanner?
|
suspend fun getTravalaPromoBanner(): PromoBanner?
|
||||||
|
|
||||||
|
suspend fun getOkxPromoBanner(): PromoBanner?
|
||||||
}
|
}
|
||||||
|
|
@ -31,4 +31,6 @@ interface QuotesRepository {
|
||||||
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote>
|
suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote>
|
||||||
|
|
||||||
suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote?
|
suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote?
|
||||||
|
|
||||||
|
suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>)
|
||||||
}
|
}
|
||||||
|
|
@ -25,4 +25,6 @@ internal class MockQuotesRepository(
|
||||||
return quotes.map { it.getOrElse { e -> throw e } }.first()
|
return quotes.map { it.getOrElse { e -> throw e } }.first()
|
||||||
.first { it.rawCurrencyId == currencyId.rawCurrencyId }
|
.first { it.rawCurrencyId == currencyId.rawCurrencyId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun fetchQuotes(currenciesIds: Set<CryptoCurrency.ID>) {}
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +104,6 @@ internal class ItemsBuilder @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val BUY_TANGEM_URL = "https://buy.tangem.com/"
|
const val BUY_TANGEM_URL = "https://buy.tangem.com/?utm_source=tangem&utm_medium=app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.send.impl.presentation
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
|
@ -60,7 +61,9 @@ internal class SendFragment : ComposeFragment() {
|
||||||
@Composable
|
@Composable
|
||||||
override fun ScreenContent(modifier: Modifier) {
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
||||||
SendScreen(viewModel.uiState, currentState.value)
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
SendScreen(uiState, currentState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,6 @@ package com.tangem.features.send.impl.presentation.viewmodel
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import arrow.core.getOrElse
|
import arrow.core.getOrElse
|
||||||
|
|
@ -126,7 +123,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val stateFactory = SendStateFactory(
|
private val stateFactory = SendStateFactory(
|
||||||
clickIntents = this,
|
clickIntents = this,
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
userWalletProvider = Provider { userWallet },
|
userWalletProvider = Provider { userWallet },
|
||||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
|
|
@ -136,7 +133,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private val recipientStateFactory = RecipientSendFactory(
|
private val recipientStateFactory = RecipientSendFactory(
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable },
|
isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable },
|
||||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||||
|
|
@ -144,14 +141,14 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private val amountStateFactory = AmountStateFactory(
|
private val amountStateFactory = AmountStateFactory(
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
)
|
)
|
||||||
|
|
||||||
private val feeStateFactory = FeeStateFactory(
|
private val feeStateFactory = FeeStateFactory(
|
||||||
clickIntents = this,
|
clickIntents = this,
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||||
|
|
@ -160,13 +157,13 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val eventStateFactory = SendEventStateFactory(
|
private val eventStateFactory = SendEventStateFactory(
|
||||||
clickIntents = this,
|
clickIntents = this,
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
feeStateFactory = feeStateFactory,
|
feeStateFactory = feeStateFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val feeNotificationFactory = FeeNotificationFactory(
|
private val feeNotificationFactory = FeeNotificationFactory(
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
clickIntents = this,
|
clickIntents = this,
|
||||||
)
|
)
|
||||||
|
|
@ -174,7 +171,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val sendNotificationFactory = SendNotificationFactory(
|
private val sendNotificationFactory = SendNotificationFactory(
|
||||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
userWalletProvider = Provider { userWallet },
|
userWalletProvider = Provider { userWallet },
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
||||||
|
|
@ -189,15 +186,16 @@ internal class SendViewModel @Inject constructor(
|
||||||
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
SendScreenAnalyticSender(
|
SendScreenAnalyticSender(
|
||||||
stateRouterProvider = Provider { stateRouter },
|
stateRouterProvider = Provider { stateRouter },
|
||||||
currentStateProvider = Provider { uiState },
|
currentStateProvider = Provider { uiState.value },
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
cryptoCurrencyProvider = Provider { cryptoCurrency },
|
cryptoCurrencyProvider = Provider { cryptoCurrency },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo convert to StateFlow
|
// todo convert to StateFlow
|
||||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
val uiState: MutableStateFlow<SendUiState> = MutableStateFlow(
|
||||||
private set
|
value = stateFactory.getInitialState(),
|
||||||
|
)
|
||||||
|
|
||||||
private var userWallet: UserWallet by Delegates.notNull()
|
private var userWallet: UserWallet by Delegates.notNull()
|
||||||
private var userWallets: List<AvailableWallet> = emptyList()
|
private var userWallets: List<AvailableWallet> = emptyList()
|
||||||
|
|
@ -274,7 +272,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
.conflate()
|
.conflate()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach {
|
||||||
uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
uiState.value = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
.saveIn(balanceHidingJobHolder)
|
.saveIn(balanceHidingJobHolder)
|
||||||
|
|
@ -347,15 +345,15 @@ internal class SendViewModel @Inject constructor(
|
||||||
feeCryptoCurrencyStatus = feeCurrencyStatus
|
feeCryptoCurrencyStatus = feeCurrencyStatus
|
||||||
subscribeOnQRScannerResult()
|
subscribeOnQRScannerResult()
|
||||||
when {
|
when {
|
||||||
uiState.sendState?.isSuccess == true -> return
|
uiState.value.sendState?.isSuccess == true -> return
|
||||||
transactionId != null && amount != null && destinationAddress != null -> {
|
transactionId != null && amount != null && destinationAddress != null -> {
|
||||||
loadFee()
|
loadFee()
|
||||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||||
stateRouter.showSend()
|
stateRouter.showSend()
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
uiState = stateFactory.getReadyState()
|
uiState.value = stateFactory.getReadyState()
|
||||||
getWalletsAndRecent()
|
getWalletsAndRecent()
|
||||||
stateRouter.showRecipient()
|
stateRouter.showRecipient()
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
|
|
@ -379,9 +377,9 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}.onSuccess { result ->
|
}.onSuccess { result ->
|
||||||
userWallets = result
|
userWallets = result
|
||||||
uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
uiState = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
|
uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = emptyList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -419,7 +417,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
pageSize = RECENT_TX_SIZE,
|
pageSize = RECENT_TX_SIZE,
|
||||||
).getOrElse { emptyList() }
|
).getOrElse { emptyList() }
|
||||||
}
|
}
|
||||||
uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
uiState.value = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onStateActive() {
|
private fun onStateActive() {
|
||||||
|
|
@ -430,7 +428,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
SendUiStateType.EditFee,
|
SendUiStateType.EditFee,
|
||||||
-> loadFee()
|
-> loadFee()
|
||||||
SendUiStateType.Send -> {
|
SendUiStateType.Send -> {
|
||||||
uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
uiState.value = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
||||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||||
}
|
}
|
||||||
else -> Unit
|
else -> Unit
|
||||||
|
|
@ -443,7 +441,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
sendNotificationFactory.create()
|
sendNotificationFactory.create()
|
||||||
.conflate()
|
.conflate()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { uiState = stateFactory.getSendNotificationState(notifications = it) }
|
.onEach { uiState.value = stateFactory.getSendNotificationState(notifications = it) }
|
||||||
.flowOn(dispatchers.main)
|
.flowOn(dispatchers.main)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
.saveIn(sendNotificationsJobHolder)
|
.saveIn(sendNotificationsJobHolder)
|
||||||
|
|
@ -453,7 +451,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
feeNotificationFactory.create()
|
feeNotificationFactory.create()
|
||||||
.conflate()
|
.conflate()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) }
|
.onEach { uiState.value = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||||
.flowOn(dispatchers.main)
|
.flowOn(dispatchers.main)
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
.saveIn(feeNotificationsJobHolder)
|
.saveIn(feeNotificationsJobHolder)
|
||||||
|
|
@ -463,7 +461,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
override fun popBackStack() = stateRouter.popBackStack()
|
override fun popBackStack() = stateRouter.popBackStack()
|
||||||
override fun onBackClick() {
|
override fun onBackClick() {
|
||||||
cancelFeeRequest()
|
cancelFeeRequest()
|
||||||
stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
|
stateRouter.onBackClick(isSuccess = uiState.value.sendState?.isSuccess == true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseClick() {
|
override fun onCloseClick() {
|
||||||
|
|
@ -479,8 +477,8 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
override fun onNextClick(isFromEdit: Boolean) {
|
override fun onNextClick(isFromEdit: Boolean) {
|
||||||
val currentState = stateRouter.currentState.value
|
val currentState = stateRouter.currentState.value
|
||||||
uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
||||||
sendScreenAnalyticSender.send(currentState.type, uiState)
|
sendScreenAnalyticSender.send(currentState.type, uiState.value)
|
||||||
when (currentState.type) {
|
when (currentState.type) {
|
||||||
SendUiStateType.Fee,
|
SendUiStateType.Fee,
|
||||||
SendUiStateType.EditFee,
|
SendUiStateType.EditFee,
|
||||||
|
|
@ -507,9 +505,9 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||||
val recipient = uiState.recipientState?.addressTextField?.value
|
val recipient = uiState.value.recipientState?.addressTextField?.value
|
||||||
val feeValue = uiState.feeState?.fee?.amount?.value
|
val feeValue = uiState.value.feeState?.fee?.amount?.value
|
||||||
val amountValue = (uiState.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
val amountValue = (uiState.value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||||
|
|
||||||
val receivingAmount = if (amountValue != null && feeValue != null) {
|
val receivingAmount = if (amountValue != null && feeValue != null) {
|
||||||
checkAndCalculateSubtractedAmount(
|
checkAndCalculateSubtractedAmount(
|
||||||
|
|
@ -517,7 +515,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
@ -539,20 +537,20 @@ internal class SendViewModel @Inject constructor(
|
||||||
innerRouter.openTokenDetails(userWalletId, currency)
|
innerRouter.openTokenDetails(userWalletId, currency)
|
||||||
|
|
||||||
private fun onFeeNext(): Boolean {
|
private fun onFeeNext(): Boolean {
|
||||||
val feeState = uiState.getFeeState(stateRouter.isEditState)
|
val feeState = uiState.value.getFeeState(stateRouter.isEditState)
|
||||||
val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false
|
||||||
if (checkIfFeeTooLow(feeSelectorState)) {
|
if (checkIfFeeTooLow(feeSelectorState)) {
|
||||||
uiState = eventStateFactory.getFeeTooLowAlert(
|
uiState.value = eventStateFactory.getFeeTooLowAlert(
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return checkIfFeeTooHigh(
|
return checkIfFeeTooHigh(
|
||||||
feeSelectorState = feeSelectorState,
|
feeSelectorState = feeSelectorState,
|
||||||
onShow = { diff ->
|
onShow = { diff ->
|
||||||
uiState = eventStateFactory.getFeeTooHighAlert(
|
uiState.value = eventStateFactory.getFeeTooHighAlert(
|
||||||
diff = diff,
|
diff = diff,
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -584,20 +582,20 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
// region amount state clicks
|
// region amount state clicks
|
||||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||||
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
uiState.value = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAmountValueChange(value: String) {
|
override fun onAmountValueChange(value: String) {
|
||||||
uiState = amountStateFactory.getOnAmountValueChange(value)
|
uiState.value = amountStateFactory.getOnAmountValueChange(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMaxValueClick() {
|
override fun onMaxValueClick() {
|
||||||
uiState = amountStateFactory.getOnMaxAmountClick()
|
uiState.value = amountStateFactory.getOnMaxAmountClick()
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked)
|
analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAmountPasteTriggerDismiss() {
|
override fun onAmountPasteTriggerDismiss() {
|
||||||
uiState = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
uiState.value = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
|
@ -606,10 +604,10 @@ internal class SendViewModel @Inject constructor(
|
||||||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
|
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!checkIfXrpAddressValue(value)) {
|
if (!checkIfXrpAddressValue(value)) {
|
||||||
uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
||||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||||
val isValidAddress = validateAddress(value)
|
val isValidAddress = validateAddress(value)
|
||||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
uiState.value = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||||
type?.let {
|
type?.let {
|
||||||
analyticsEventHandler.send(
|
analyticsEventHandler.send(
|
||||||
SendAnalyticEvents.AddressEntered(
|
SendAnalyticEvents.AddressEntered(
|
||||||
|
|
@ -626,11 +624,11 @@ internal class SendViewModel @Inject constructor(
|
||||||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
if (!checkIfXrpAddressValue(value)) {
|
if (!checkIfXrpAddressValue(value)) {
|
||||||
uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
uiState.value = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
||||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||||
val recipientState = uiState.getRecipientState(stateRouter.isEditState)
|
val recipientState = uiState.value.getRecipientState(stateRouter.isEditState)
|
||||||
val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
|
val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
|
||||||
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
uiState.value = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
||||||
}
|
}
|
||||||
}.saveIn(memoValidationJobHolder)
|
}.saveIn(memoValidationJobHolder)
|
||||||
}
|
}
|
||||||
|
|
@ -652,21 +650,22 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
|
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
|
||||||
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
|
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
|
||||||
uiState =
|
uiState.value =
|
||||||
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
|
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
|
||||||
uiState = recipientStateFactory.getOnXAddressMemoState()
|
uiState.value = recipientStateFactory.getOnXAddressMemoState()
|
||||||
val isValidAddress = validateAddress(decodedAddress.address)
|
val isValidAddress = validateAddress(decodedAddress.address)
|
||||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
uiState.value =
|
||||||
|
recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEnteredValidAddress(isNotValid: Boolean) {
|
private fun onEnteredValidAddress(isNotValid: Boolean) {
|
||||||
uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
uiState.value = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
|
private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) {
|
||||||
val memo = uiState.getRecipientState(stateRouter.isEditState)?.memoTextField?.value
|
val memo = uiState.value.getRecipientState(stateRouter.isEditState)?.memoTextField?.value
|
||||||
val isValidMemo = validateMemo(memo)
|
val isValidMemo = validateMemo(memo)
|
||||||
|
|
||||||
val isRecent = type == EnterAddressSource.RecentAddress
|
val isRecent = type == EnterAddressSource.RecentAddress
|
||||||
|
|
@ -678,7 +677,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
override fun feeReload() = loadFee()
|
override fun feeReload() = loadFee()
|
||||||
|
|
||||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
uiState.value = feeStateFactory.onFeeSelectedState(feeType)
|
||||||
updateFeeNotifications()
|
updateFeeNotifications()
|
||||||
if (feeType == FeeType.Custom) {
|
if (feeType == FeeType.Custom) {
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
|
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
|
||||||
|
|
@ -686,7 +685,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||||
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
|
uiState.value = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||||
updateFeeNotifications()
|
updateFeeNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -702,14 +701,14 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun loadFee() {
|
private fun loadFee() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isShowStatus = uiState.feeState?.fee == null
|
val isShowStatus = uiState.value.feeState?.fee == null
|
||||||
if (isShowStatus) {
|
if (isShowStatus) {
|
||||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
uiState.value = feeStateFactory.onFeeOnLoadingState()
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
}
|
}
|
||||||
val result = callFeeUseCase()?.fold(
|
val result = callFeeUseCase()?.fold(
|
||||||
ifRight = {
|
ifRight = {
|
||||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
uiState.value = feeStateFactory.onFeeOnLoadedState(it)
|
||||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||||
},
|
},
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
|
|
@ -725,7 +724,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeeLoadFailed(isShowStatus: Boolean) {
|
private fun onFeeLoadFailed(isShowStatus: Boolean) {
|
||||||
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
|
if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkIfSubtractAvailable() {
|
private suspend fun checkIfSubtractAvailable() {
|
||||||
|
|
@ -744,8 +743,8 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
||||||
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
|
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
|
||||||
val amountState = uiState.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null
|
val amountState = uiState.value.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null
|
||||||
val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null
|
val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null
|
||||||
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
|
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
|
||||||
|
|
||||||
return getFeeUseCase.invoke(
|
return getFeeUseCase.invoke(
|
||||||
|
|
@ -759,10 +758,10 @@ internal class SendViewModel @Inject constructor(
|
||||||
|
|
||||||
// region send state clicks
|
// region send state clicks
|
||||||
override fun onSendClick() {
|
override fun onSendClick() {
|
||||||
val sendState = uiState.sendState ?: return
|
val sendState = uiState.value.sendState ?: return
|
||||||
if (sendState.isSuccess) popBackStack()
|
if (sendState.isSuccess) popBackStack()
|
||||||
|
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||||
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
||||||
verifyAndSendTransaction()
|
verifyAndSendTransaction()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -772,22 +771,22 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAmount() {
|
override fun showAmount() {
|
||||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||||
stateRouter.showAmount(isFromConfirmation = true)
|
stateRouter.showAmount(isFromConfirmation = true)
|
||||||
setNeverToShowTapHelp()
|
setNeverToShowTapHelp()
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showRecipient() {
|
override fun showRecipient() {
|
||||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||||
stateRouter.showRecipient(isFromConfirmation = true)
|
stateRouter.showRecipient(isFromConfirmation = true)
|
||||||
uiState = stateFactory.getHiddenTapHelpState()
|
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||||
setNeverToShowTapHelp()
|
setNeverToShowTapHelp()
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showFee() {
|
override fun showFee() {
|
||||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||||
stateRouter.showFee(isFromConfirmation = true)
|
stateRouter.showFee(isFromConfirmation = true)
|
||||||
setNeverToShowTapHelp()
|
setNeverToShowTapHelp()
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
||||||
|
|
@ -798,7 +797,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExploreClick() {
|
override fun onExploreClick() {
|
||||||
val sendState = uiState.sendState ?: return
|
val sendState = uiState.value.sendState ?: return
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
||||||
innerRouter.openUrl(sendState.txUrl)
|
innerRouter.openUrl(sendState.txUrl)
|
||||||
}
|
}
|
||||||
|
|
@ -813,7 +812,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
reduceAmountTo: BigDecimal?,
|
reduceAmountTo: BigDecimal?,
|
||||||
clazz: Class<out SendNotification>,
|
clazz: Class<out SendNotification>,
|
||||||
) {
|
) {
|
||||||
uiState = when {
|
uiState.value = when {
|
||||||
reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState(
|
reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState(
|
||||||
reduceAmountBy = reduceAmountBy,
|
reduceAmountBy = reduceAmountBy,
|
||||||
reduceAmountByDiff = reduceAmountByDiff,
|
reduceAmountByDiff = reduceAmountByDiff,
|
||||||
|
|
@ -822,20 +821,22 @@ internal class SendViewModel @Inject constructor(
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
|
||||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
uiState.value = sendNotificationFactory.dismissNotificationState(clazz)
|
||||||
updateNotifications()
|
updateNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
override fun onNotificationCancel(clazz: Class<out SendNotification>) {
|
||||||
uiState = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
uiState.value = sendNotificationFactory.dismissNotificationState(clazz = clazz, isIgnored = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyAndSendTransaction() {
|
private fun verifyAndSendTransaction() {
|
||||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
val recipient = uiState.value.recipientState?.addressTextField?.value ?: return
|
||||||
val feeState = uiState.feeState ?: return
|
val feeState = uiState.value.feeState ?: return
|
||||||
val fee = feeState.fee ?: return
|
val fee = feeState.fee ?: return
|
||||||
val memo = uiState.recipientState?.memoTextField?.value
|
val memo = uiState.value.recipientState?.memoTextField?.value
|
||||||
val amountValue = (uiState.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: return
|
val amountValue = (uiState.value.amountState as? AmountState.Data)
|
||||||
|
?.amountTextField?.cryptoAmount?.value
|
||||||
|
?: return
|
||||||
val feeValue = fee.amount.value ?: return
|
val feeValue = fee.amount.value ?: return
|
||||||
|
|
||||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||||
|
|
@ -843,7 +844,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||||
amountValue = amountValue,
|
amountValue = amountValue,
|
||||||
feeValue = feeValue,
|
feeValue = feeValue,
|
||||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
|
@ -857,10 +858,10 @@ internal class SendViewModel @Inject constructor(
|
||||||
).fold(
|
).fold(
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
Timber.e(it)
|
Timber.e(it)
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
uiState = eventStateFactory.getGenericErrorState(
|
uiState.value = eventStateFactory.getGenericErrorState(
|
||||||
error = it,
|
error = it,
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
ifRight = { txData ->
|
ifRight = { txData ->
|
||||||
|
|
@ -871,21 +872,23 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
||||||
sendTransactionUseCase(
|
val result = sendTransactionUseCase(
|
||||||
txData = txData,
|
txData = txData,
|
||||||
userWallet = userWallet,
|
userWallet = userWallet,
|
||||||
network = cryptoCurrency.network,
|
network = cryptoCurrency.network,
|
||||||
).fold(
|
)
|
||||||
|
|
||||||
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
|
|
||||||
|
result.fold(
|
||||||
ifLeft = { error ->
|
ifLeft = { error ->
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState.value = eventStateFactory.getSendTransactionErrorState(
|
||||||
uiState = eventStateFactory.getSendTransactionErrorState(
|
|
||||||
error = error,
|
error = error,
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
|
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
|
||||||
},
|
},
|
||||||
ifRight = {
|
ifRight = {
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
|
||||||
updateTransactionStatus(txData)
|
updateTransactionStatus(txData)
|
||||||
addTokenToWalletIfNeeded()
|
addTokenToWalletIfNeeded()
|
||||||
scheduleUpdates()
|
scheduleUpdates()
|
||||||
|
|
@ -897,7 +900,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
private fun addTokenToWalletIfNeeded() {
|
private fun addTokenToWalletIfNeeded() {
|
||||||
if (cryptoCurrency !is CryptoCurrency.Token) return
|
if (cryptoCurrency !is CryptoCurrency.Token) return
|
||||||
|
|
||||||
val recipientState = uiState.getRecipientState(stateRouter.isEditState) ?: return
|
val recipientState = uiState.value.getRecipientState(stateRouter.isEditState) ?: return
|
||||||
val destinationAddress = recipientState.addressTextField.value
|
val destinationAddress = recipientState.addressTextField.value
|
||||||
|
|
||||||
val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return
|
val receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return
|
||||||
|
|
@ -916,7 +919,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
network = cryptoCurrency.network,
|
network = cryptoCurrency.network,
|
||||||
).getOrElse { "" }
|
).getOrElse { "" }
|
||||||
uiState = stateFactory.getTransactionSendState(txData, txUrl)
|
uiState.value = stateFactory.getTransactionSendState(txData, txUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleUpdates() {
|
private fun scheduleUpdates() {
|
||||||
|
|
@ -934,7 +937,7 @@ internal class SendViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCheckFeeUpdate() {
|
private fun onCheckFeeUpdate() {
|
||||||
val sendState = uiState.sendState ?: return
|
val sendState = uiState.value.sendState ?: return
|
||||||
val isSuccess = sendState.isSuccess
|
val isSuccess = sendState.isSuccess
|
||||||
val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error }
|
val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error }
|
||||||
|
|
||||||
|
|
@ -942,30 +945,30 @@ internal class SendViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val feeUpdatedState = callFeeUseCase()?.fold(
|
val feeUpdatedState = callFeeUseCase()?.fold(
|
||||||
ifRight = {
|
ifRight = {
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
eventStateFactory.getFeeUpdatedAlert(
|
eventStateFactory.getFeeUpdatedAlert(
|
||||||
fee = it,
|
fee = it,
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
onFeeNotIncreased = {
|
onFeeNotIncreased = {
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||||
verifyAndSendTransaction()
|
verifyAndSendTransaction()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
eventStateFactory.getFeeUnreachableErrorState(
|
eventStateFactory.getFeeUnreachableErrorState(
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
uiState = if (feeUpdatedState != null) {
|
uiState.value = if (feeUpdatedState != null) {
|
||||||
feeUpdatedState
|
feeUpdatedState
|
||||||
} else {
|
} else {
|
||||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||||
eventStateFactory.getFeeUnreachableErrorState(
|
eventStateFactory.getFeeUnreachableErrorState(
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -976,12 +979,12 @@ internal class SendViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
neverShowTapHelpUseCase()
|
neverShowTapHelpUseCase()
|
||||||
}
|
}
|
||||||
uiState = stateFactory.getHiddenTapHelpState()
|
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showErrorAlert() {
|
private fun showErrorAlert() {
|
||||||
uiState = eventStateFactory.getGenericErrorState(
|
uiState.value = eventStateFactory.getGenericErrorState(
|
||||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
||||||
imageLarge = exchangeProvider.imageLargeUrl,
|
imageLarge = exchangeProvider.imageLargeUrl,
|
||||||
termsOfUse = exchangeProvider.termsOfUse,
|
termsOfUse = exchangeProvider.termsOfUse,
|
||||||
privacyPolicy = exchangeProvider.privacyPolicy,
|
privacyPolicy = exchangeProvider.privacyPolicy,
|
||||||
|
isRecommended = exchangeProvider.isRecommended,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +62,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
||||||
imageLarge = additionalProvider.imageLargeUrl,
|
imageLarge = additionalProvider.imageLargeUrl,
|
||||||
termsOfUse = additionalProvider.termsOfUse,
|
termsOfUse = additionalProvider.termsOfUse,
|
||||||
privacyPolicy = additionalProvider.privacyPolicy,
|
privacyPolicy = additionalProvider.privacyPolicy,
|
||||||
|
isRecommended = additionalProvider.isRecommended,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,6 +70,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
||||||
return when (type) {
|
return when (type) {
|
||||||
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
|
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
|
||||||
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
|
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
|
||||||
|
ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ data class ExchangeStatusModel(
|
||||||
enum class ExchangeStatus {
|
enum class ExchangeStatus {
|
||||||
New,
|
New,
|
||||||
Waiting,
|
Waiting,
|
||||||
|
WaitingTxHash,
|
||||||
Confirming,
|
Confirming,
|
||||||
Verifying,
|
Verifying,
|
||||||
Exchanging,
|
Exchanging,
|
||||||
|
|
@ -19,4 +20,5 @@ enum class ExchangeStatus {
|
||||||
Finished,
|
Finished,
|
||||||
Refunded,
|
Refunded,
|
||||||
Cancelled,
|
Cancelled,
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
@ -26,6 +26,7 @@ data class SavedSwapTransactionListModelInner(
|
||||||
val transactions: List<SavedSwapTransactionModel>,
|
val transactions: List<SavedSwapTransactionModel>,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO refactor to use separate models to store
|
||||||
data class SavedSwapTransactionModel(
|
data class SavedSwapTransactionModel(
|
||||||
val txId: String,
|
val txId: String,
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ data class CryptoCurrencySwapInfo(
|
||||||
*
|
*
|
||||||
* @property providerId provider id
|
* @property providerId provider id
|
||||||
* @property rateTypes supported rate types
|
* @property rateTypes supported rate types
|
||||||
|
* @property isRecommended flag that indicates if this provider is recommended
|
||||||
|
*
|
||||||
|
* Uses to store transaction data in datastore, when extends - should always add default value
|
||||||
|
* to support backward compatibility
|
||||||
*/
|
*/
|
||||||
data class SwapProvider(
|
data class SwapProvider(
|
||||||
val providerId: String,
|
val providerId: String,
|
||||||
|
|
@ -34,11 +38,13 @@ data class SwapProvider(
|
||||||
val imageLarge: String,
|
val imageLarge: String,
|
||||||
val termsOfUse: String?,
|
val termsOfUse: String?,
|
||||||
val privacyPolicy: String?,
|
val privacyPolicy: String?,
|
||||||
|
val isRecommended: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ExchangeProviderType {
|
enum class ExchangeProviderType(val providerName: String) {
|
||||||
DEX,
|
DEX("DEX"),
|
||||||
CEX,
|
CEX("CEX"),
|
||||||
|
DEX_BRIDGE("DEX/Bridge"),
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.extenstions.unwrap
|
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
import com.tangem.domain.demo.DemoConfig
|
import com.tangem.domain.demo.DemoConfig
|
||||||
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||||
import com.tangem.domain.tokens.model.*
|
import com.tangem.domain.tokens.model.*
|
||||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||||
|
|
@ -60,6 +61,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||||
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||||
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val quotesRepository: QuotesRepository,
|
private val quotesRepository: QuotesRepository,
|
||||||
private val swapTransactionRepository: SwapTransactionRepository,
|
private val swapTransactionRepository: SwapTransactionRepository,
|
||||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||||
|
|
@ -275,7 +277,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
|
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
|
||||||
val networkId = fromToken.currency.network.backendId
|
val networkId = fromToken.currency.network.backendId
|
||||||
when (provider.type) {
|
when (provider.type) {
|
||||||
ExchangeProviderType.DEX -> {
|
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||||
manageDex(
|
manageDex(
|
||||||
networkId = networkId,
|
networkId = networkId,
|
||||||
fromToken = fromToken,
|
fromToken = fromToken,
|
||||||
|
|
@ -349,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
provider to getQuotesState(
|
provider to getQuotesState(
|
||||||
exchangeProviderType = ExchangeProviderType.DEX,
|
exchangeProviderType = provider.type,
|
||||||
quoteDataModel = quotes,
|
quoteDataModel = quotes,
|
||||||
amount = amount,
|
amount = amount,
|
||||||
fromToken = fromToken,
|
fromToken = fromToken,
|
||||||
|
|
@ -420,13 +422,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
fromToken.network.backendId,
|
fromToken.network.backendId,
|
||||||
fromToken.network.derivationPath.value,
|
fromToken.network.derivationPath.value,
|
||||||
) ?: ProxyAmount.empty()
|
) ?: ProxyAmount.empty()
|
||||||
|
// ignore if amount is bigger than balance
|
||||||
|
if (amount.value > nativeBalance.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val fee = when (txFee) {
|
val fee = when (txFee) {
|
||||||
TxFeeState.Empty -> BigDecimal.ZERO
|
TxFeeState.Empty -> BigDecimal.ZERO
|
||||||
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
|
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
|
||||||
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
|
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
|
||||||
}
|
}
|
||||||
val minAvailableAmount = nativeBalance.value - existentialDeposit - fee
|
val minAvailableAmount = nativeBalance.value - existentialDeposit - fee
|
||||||
if (nativeBalance.value.minus(amount.value) < existentialDeposit) {
|
if (nativeBalance.value.minus(amount.value + fee) < existentialDeposit) {
|
||||||
warnings.add(Warning.ExistentialDepositWarning(existentialDeposit, minAvailableAmount))
|
warnings.add(Warning.ExistentialDepositWarning(existentialDeposit, minAvailableAmount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -568,6 +574,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
includeFeeInAmount: IncludeFeeInAmount,
|
includeFeeInAmount: IncludeFeeInAmount,
|
||||||
fee: TxFee,
|
fee: TxFee,
|
||||||
): SwapTransactionState {
|
): SwapTransactionState {
|
||||||
|
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
|
||||||
|
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
|
||||||
|
|
||||||
return when (swapProvider.type) {
|
return when (swapProvider.type) {
|
||||||
ExchangeProviderType.CEX -> {
|
ExchangeProviderType.CEX -> {
|
||||||
val amountDecimal = toBigDecimalOrNull(amountToSwap)
|
val amountDecimal = toBigDecimalOrNull(amountToSwap)
|
||||||
|
|
@ -586,8 +595,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
userWalletId = requireNotNull(getSelectedWallet()).walletId,
|
userWalletId = requireNotNull(getSelectedWallet()).walletId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ExchangeProviderType.DEX -> {
|
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||||
onSwapDex(
|
onSwapDex(
|
||||||
|
provider = swapProvider,
|
||||||
networkId = currencyToSend.currency.network.backendId,
|
networkId = currencyToSend.currency.network.backendId,
|
||||||
swapData = requireNotNull(swapData),
|
swapData = requireNotNull(swapData),
|
||||||
currencyToSendStatus = currencyToSend,
|
currencyToSendStatus = currencyToSend,
|
||||||
|
|
@ -639,6 +649,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onSwapDex(
|
private suspend fun onSwapDex(
|
||||||
|
provider: SwapProvider,
|
||||||
networkId: String,
|
networkId: String,
|
||||||
swapData: SwapDataModel,
|
swapData: SwapDataModel,
|
||||||
currencyToSendStatus: CryptoCurrencyStatus,
|
currencyToSendStatus: CryptoCurrencyStatus,
|
||||||
|
|
@ -650,7 +661,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
||||||
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
|
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
|
||||||
val derivationPath = currencyToSendStatus.currency.network.derivationPath.value
|
val derivationPath = currencyToSendStatus.currency.network.derivationPath.value
|
||||||
val dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData
|
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
|
||||||
|
val dataToSign = dexTransaction.txData
|
||||||
val txData = createTransactionUseCase(
|
val txData = createTransactionUseCase(
|
||||||
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
|
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
|
||||||
fee = getFeeForTransaction(
|
fee = getFeeForTransaction(
|
||||||
|
|
@ -684,6 +696,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
txHash = txHash,
|
txHash = txHash,
|
||||||
payInExtraId = swapData.transaction.txExtraId,
|
payInExtraId = swapData.transaction.txExtraId,
|
||||||
)
|
)
|
||||||
|
if (provider.type == ExchangeProviderType.DEX_BRIDGE) {
|
||||||
|
val timestamp = System.currentTimeMillis()
|
||||||
|
storeSwapTransaction(
|
||||||
|
currencyToSend = currencyToSendStatus,
|
||||||
|
currencyToGet = currencyToGetStatus,
|
||||||
|
amount = amount,
|
||||||
|
swapProvider = provider,
|
||||||
|
swapDataModel = swapData,
|
||||||
|
timestamp = timestamp,
|
||||||
|
)
|
||||||
|
}
|
||||||
storeLastCryptoCurrencyId(currencyToGetStatus.currency)
|
storeLastCryptoCurrencyId(currencyToGetStatus.currency)
|
||||||
SwapTransactionState.TxSent(
|
SwapTransactionState.TxSent(
|
||||||
fromAmount = amountFormatter.formatSwapAmountToUI(
|
fromAmount = amountFormatter.formatSwapAmountToUI(
|
||||||
|
|
@ -700,16 +723,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
ifLeft = {
|
ifLeft = { handleSendTxError(it) },
|
||||||
when (it) {
|
|
||||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
|
||||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
|
||||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
|
||||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
|
||||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
|
||||||
else -> SwapTransactionState.UnknownError
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -781,14 +795,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
val derivationPath = currencyToSend.currency.network.derivationPath.value
|
val derivationPath = currencyToSend.currency.network.derivationPath.value
|
||||||
return result.fold(
|
return result.fold(
|
||||||
ifLeft = {
|
ifLeft = {
|
||||||
when (it) {
|
handleSendTxError(it)
|
||||||
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
|
||||||
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
|
||||||
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
|
||||||
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
|
||||||
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
|
||||||
else -> SwapTransactionState.UnknownError
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ifRight = { txHash ->
|
ifRight = { txHash ->
|
||||||
repository.exchangeSent(
|
repository.exchangeSent(
|
||||||
|
|
@ -834,6 +841,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState {
|
||||||
|
return when (txError) {
|
||||||
|
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
|
||||||
|
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
|
||||||
|
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
|
||||||
|
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
|
||||||
|
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
|
||||||
|
else -> SwapTransactionState.UnknownError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
|
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
|
||||||
val feeAmountValue = fee.feeValue
|
val feeAmountValue = fee.feeValue
|
||||||
val feeAmount = Amount(
|
val feeAmount = Amount(
|
||||||
|
|
@ -882,11 +900,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
swapProvider: SwapProvider,
|
swapProvider: SwapProvider,
|
||||||
swapDataModel: SwapDataModel,
|
swapDataModel: SwapDataModel,
|
||||||
timestamp: Long,
|
timestamp: Long,
|
||||||
txExternalUrl: String,
|
txExternalUrl: String? = null,
|
||||||
txExternalId: String,
|
txExternalId: String? = null,
|
||||||
) {
|
) {
|
||||||
|
val selectedWallet = getSelectedWallet() ?: return
|
||||||
swapTransactionRepository.storeTransaction(
|
swapTransactionRepository.storeTransaction(
|
||||||
userWalletId = UserWalletId(userWalletManager.getWalletId()),
|
userWalletId = selectedWallet.walletId,
|
||||||
fromCryptoCurrency = currencyToSend.currency,
|
fromCryptoCurrency = currencyToSend.currency,
|
||||||
toCryptoCurrency = currencyToGet.currency,
|
toCryptoCurrency = currencyToGet.currency,
|
||||||
transaction = SavedSwapTransactionModel(
|
transaction = SavedSwapTransactionModel(
|
||||||
|
|
@ -1072,7 +1091,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
when (exchangeProviderType) {
|
when (exchangeProviderType) {
|
||||||
ExchangeProviderType.DEX -> {
|
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||||
val state = updatePermissionState(
|
val state = updatePermissionState(
|
||||||
networkId = networkId,
|
networkId = networkId,
|
||||||
fromTokenStatus = fromToken,
|
fromTokenStatus = fromToken,
|
||||||
|
|
@ -1407,19 +1426,25 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
swapAmount = swapAmount,
|
swapAmount = swapAmount,
|
||||||
spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" },
|
spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" },
|
||||||
)
|
)
|
||||||
val feeData = try {
|
val userWallet = getSelectedWallet()
|
||||||
transactionManager.getFee(
|
val cardId = userWallet?.scanResponse?.card?.cardId
|
||||||
networkId = networkId,
|
val feeData = if (cardId != null && isDemoCardUseCase(cardId)) {
|
||||||
amountToSend = BigDecimal.ZERO,
|
getDemoFees(fromTokenStatus.currency)
|
||||||
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
} else {
|
||||||
destinationAddress = fromToken.getContractAddress(),
|
try {
|
||||||
increaseBy = INCREASE_GAS_LIMIT_BY,
|
transactionManager.getFee(
|
||||||
data = transactionData,
|
networkId = networkId,
|
||||||
derivationPath = derivationPath,
|
amountToSend = BigDecimal.ZERO,
|
||||||
)
|
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
||||||
} catch (e: Exception) {
|
destinationAddress = fromToken.getContractAddress(),
|
||||||
Timber.e(e, "Failed to get fee")
|
increaseBy = INCREASE_GAS_LIMIT_BY,
|
||||||
null
|
data = transactionData,
|
||||||
|
derivationPath = derivationPath,
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to get fee")
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val feeState = feeData?.let {
|
val feeState = feeData?.let {
|
||||||
when (feeData) {
|
when (feeData) {
|
||||||
|
|
@ -1803,10 +1828,37 @@ internal class SwapInteractorImpl @Inject constructor(
|
||||||
.toMap()
|
.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees {
|
||||||
|
val demoFee = ProxyAmount(
|
||||||
|
currencySymbol = cryptoCurrency.symbol,
|
||||||
|
value = minDemoFee,
|
||||||
|
decimals = cryptoCurrency.decimals,
|
||||||
|
)
|
||||||
|
return ProxyFees.MultipleFees(
|
||||||
|
minFee = ProxyFee.Common(
|
||||||
|
gasLimit = 1.toBigInteger(),
|
||||||
|
fee = demoFee,
|
||||||
|
),
|
||||||
|
normalFee = ProxyFee.Common(
|
||||||
|
gasLimit = 1.toBigInteger(),
|
||||||
|
fee = demoFee.copy(value = normalDemoFee),
|
||||||
|
|
||||||
|
),
|
||||||
|
priorityFee = ProxyFee.Common(
|
||||||
|
gasLimit = 1.toBigInteger(),
|
||||||
|
fee = demoFee.copy(value = priorityDemoFee),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Suppress("UnusedPrivateMember")
|
@Suppress("UnusedPrivateMember")
|
||||||
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
|
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
|
||||||
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
||||||
private const val INFINITY_SYMBOL = "∞"
|
private const val INFINITY_SYMBOL = "∞"
|
||||||
|
|
||||||
|
private val minDemoFee = "0.0001".toBigDecimal()
|
||||||
|
private val normalDemoFee = "0.0002".toBigDecimal()
|
||||||
|
private val priorityDemoFee = "0.0003".toBigDecimal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +43,7 @@ class SwapDomainModule {
|
||||||
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
|
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
|
||||||
@SwapScope createTransactionUseCase: CreateTransactionUseCase,
|
@SwapScope createTransactionUseCase: CreateTransactionUseCase,
|
||||||
createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||||
|
isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
quotesRepository: QuotesRepository,
|
quotesRepository: QuotesRepository,
|
||||||
swapTransactionRepository: SwapTransactionRepository,
|
swapTransactionRepository: SwapTransactionRepository,
|
||||||
appCurrencyRepository: AppCurrencyRepository,
|
appCurrencyRepository: AppCurrencyRepository,
|
||||||
|
|
@ -62,6 +63,7 @@ class SwapDomainModule {
|
||||||
sendTransactionUseCase = sendTransactionUseCase,
|
sendTransactionUseCase = sendTransactionUseCase,
|
||||||
createTransactionUseCase = createTransactionUseCase,
|
createTransactionUseCase = createTransactionUseCase,
|
||||||
createTransactionExtrasUseCase = createTransactionDataExtrasUseCase,
|
createTransactionExtrasUseCase = createTransactionDataExtrasUseCase,
|
||||||
|
isDemoCardUseCase = isDemoCardUseCase,
|
||||||
quotesRepository = quotesRepository,
|
quotesRepository = quotesRepository,
|
||||||
swapTransactionRepository = swapTransactionRepository,
|
swapTransactionRepository = swapTransactionRepository,
|
||||||
appCurrencyRepository = appCurrencyRepository,
|
appCurrencyRepository = appCurrencyRepository,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||||
import com.tangem.feature.swap.domain.models.ui.FeeType
|
import com.tangem.feature.swap.domain.models.ui.FeeType
|
||||||
|
|
||||||
private const val SWAP_CATEGORY = "Swap"
|
private const val SWAP_CATEGORY = "Swap"
|
||||||
|
private const val PROMO_CATEGORY = "Promo"
|
||||||
|
|
||||||
sealed class SwapEvents(
|
sealed class SwapEvents(
|
||||||
event: String,
|
event: String,
|
||||||
|
|
@ -17,7 +18,7 @@ sealed class SwapEvents(
|
||||||
params = mapOf("Token" to token),
|
params = mapOf("Token" to token),
|
||||||
)
|
)
|
||||||
|
|
||||||
object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
|
data object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked")
|
||||||
|
|
||||||
data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents(
|
data class ChooseTokenScreenOpened(val availableTokens: Boolean) : SwapEvents(
|
||||||
event = "Choose Token Screen Opened",
|
event = "Choose Token Screen Opened",
|
||||||
|
|
@ -37,7 +38,7 @@ sealed class SwapEvents(
|
||||||
params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken),
|
params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken),
|
||||||
)
|
)
|
||||||
|
|
||||||
object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission")
|
data object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission")
|
||||||
|
|
||||||
data class ButtonPermissionApproveClicked(
|
data class ButtonPermissionApproveClicked(
|
||||||
val sendToken: String,
|
val sendToken: String,
|
||||||
|
|
@ -52,9 +53,9 @@ sealed class SwapEvents(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel")
|
data object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel")
|
||||||
|
|
||||||
object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe")
|
data object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe")
|
||||||
|
|
||||||
data class SwapInProgressScreen(
|
data class SwapInProgressScreen(
|
||||||
val provider: SwapProvider,
|
val provider: SwapProvider,
|
||||||
|
|
@ -71,7 +72,7 @@ sealed class SwapEvents(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
object ProviderClicked : SwapEvents("Provider Clicked")
|
data object ProviderClicked : SwapEvents("Provider Clicked")
|
||||||
|
|
||||||
data class ProviderChosen(val provider: SwapProvider) : SwapEvents(
|
data class ProviderChosen(val provider: SwapProvider) : SwapEvents(
|
||||||
event = "Provider Chosen",
|
event = "Provider Chosen",
|
||||||
|
|
@ -88,7 +89,7 @@ sealed class SwapEvents(
|
||||||
params = mapOf("Token" to token),
|
params = mapOf("Token" to token),
|
||||||
)
|
)
|
||||||
|
|
||||||
object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap")
|
data object NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap")
|
||||||
|
|
||||||
data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents(
|
data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents(
|
||||||
event = "Notice - Not Enough Fee",
|
event = "Notice - Not Enough Fee",
|
||||||
|
|
@ -110,4 +111,20 @@ sealed class SwapEvents(
|
||||||
"Error code" to errorCode.toString(),
|
"Error code" to errorCode.toString(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// region Promo activity
|
||||||
|
data class ChangellyActivity(
|
||||||
|
val promoState: PromoState,
|
||||||
|
) : AnalyticsEvent(
|
||||||
|
category = PROMO_CATEGORY,
|
||||||
|
event = "Changelly Activity",
|
||||||
|
params = mapOf(
|
||||||
|
"State" to promoState.name,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
sealed class PromoState(val name: String) {
|
||||||
|
data object Native : PromoState("Native")
|
||||||
|
data object Recommended : PromoState("Recommended")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -44,9 +44,10 @@ sealed class ProviderState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed class AdditionalBadge {
|
sealed class AdditionalBadge {
|
||||||
object BestTrade : AdditionalBadge()
|
data object BestTrade : AdditionalBadge()
|
||||||
object Empty : AdditionalBadge()
|
data object Empty : AdditionalBadge()
|
||||||
object PermissionRequired : AdditionalBadge()
|
data object PermissionRequired : AdditionalBadge()
|
||||||
|
data object Recommended : AdditionalBadge()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -54,6 +55,7 @@ sealed class ProviderState {
|
||||||
NONE, CLICK, SELECT
|
NONE, CLICK, SELECT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefix will be disabled in 5.12 but mechanism is still implemented
|
||||||
@Immutable
|
@Immutable
|
||||||
enum class PrefixType {
|
enum class PrefixType {
|
||||||
NONE, PROVIDED_BY
|
NONE, PROVIDED_BY
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Be careful when will replace with InputRowBestRate, because RecommendedBadge was added
|
||||||
@Deprecated("Replace with InputRowBestRate")
|
@Deprecated("Replace with InputRowBestRate")
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -142,16 +143,12 @@ private fun ProviderContentState(
|
||||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val badgeModifier = Modifier.padding(start = TangemTheme.dimens.spacing4)
|
||||||
when (state.additionalBadge) {
|
when (state.additionalBadge) {
|
||||||
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(
|
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier)
|
||||||
Modifier.padding(start = TangemTheme.dimens.spacing4),
|
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier)
|
||||||
)
|
ProviderState.AdditionalBadge.Recommended -> RecommendedItem(badgeModifier)
|
||||||
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(
|
ProviderState.AdditionalBadge.Empty -> Unit
|
||||||
Modifier.padding(start = TangemTheme.dimens.spacing4),
|
|
||||||
)
|
|
||||||
ProviderState.AdditionalBadge.Empty -> {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -385,6 +382,23 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecommendedItem(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.background(
|
||||||
|
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||||
|
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.express_provider_recommended),
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.icon.accent,
|
||||||
|
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// region Preview
|
// region Preview
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
|
|
||||||
|
|
@ -677,6 +677,7 @@ internal class StateBuilder(
|
||||||
return when (dataError) {
|
return when (dataError) {
|
||||||
is DataError.ExchangeTooSmallAmountError -> {
|
is DataError.ExchangeTooSmallAmountError -> {
|
||||||
swapProvider.convertToAvailableFromProviderState(
|
swapProvider.convertToAvailableFromProviderState(
|
||||||
|
swapProvider = swapProvider,
|
||||||
alertText = resourceReference(
|
alertText = resourceReference(
|
||||||
R.string.express_provider_min_amount,
|
R.string.express_provider_min_amount,
|
||||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||||
|
|
@ -687,6 +688,7 @@ internal class StateBuilder(
|
||||||
}
|
}
|
||||||
is DataError.ExchangeTooBigAmountError -> {
|
is DataError.ExchangeTooBigAmountError -> {
|
||||||
swapProvider.convertToAvailableFromProviderState(
|
swapProvider.convertToAvailableFromProviderState(
|
||||||
|
swapProvider = swapProvider,
|
||||||
alertText = resourceReference(
|
alertText = resourceReference(
|
||||||
R.string.express_provider_max_amount,
|
R.string.express_provider_max_amount,
|
||||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||||
|
|
@ -1005,13 +1007,15 @@ internal class StateBuilder(
|
||||||
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
|
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
|
||||||
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
|
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
|
||||||
|
|
||||||
|
val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName ||
|
||||||
|
providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName
|
||||||
return uiState.copy(
|
return uiState.copy(
|
||||||
successState = SwapSuccessStateHolder(
|
successState = SwapSuccessStateHolder(
|
||||||
timestamp = swapTransactionState.timestamp,
|
timestamp = swapTransactionState.timestamp,
|
||||||
txUrl = txUrl,
|
txUrl = txUrl,
|
||||||
providerName = stringReference(providerState.name),
|
providerName = stringReference(providerState.name),
|
||||||
providerType = stringReference(providerState.type),
|
providerType = stringReference(providerState.type),
|
||||||
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
|
showStatusButton = shouldShowStatus,
|
||||||
providerIcon = providerState.iconUrl,
|
providerIcon = providerState.iconUrl,
|
||||||
rate = providerState.subtitle,
|
rate = providerState.subtitle,
|
||||||
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
|
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
|
||||||
|
|
@ -1093,7 +1097,7 @@ internal class StateBuilder(
|
||||||
): SwapStateHolder {
|
): SwapStateHolder {
|
||||||
val message = when (providerType) {
|
val message = when (providerType) {
|
||||||
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
|
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
|
||||||
ExchangeProviderType.DEX -> {
|
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||||
val refs = buildList {
|
val refs = buildList {
|
||||||
if (isPriceImpact) {
|
if (isPriceImpact) {
|
||||||
add(resourceReference(R.string.swapping_high_price_impact_description))
|
add(resourceReference(R.string.swapping_high_price_impact_description))
|
||||||
|
|
@ -1540,7 +1544,9 @@ internal class StateBuilder(
|
||||||
val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol
|
val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol
|
||||||
val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol
|
val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol
|
||||||
val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol"
|
val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol"
|
||||||
val badge = if (isNeedBestRateBadge && isBestRate) {
|
val badge = if (isRecommended) {
|
||||||
|
ProviderState.AdditionalBadge.Recommended
|
||||||
|
} else if (isNeedBestRateBadge && isBestRate) {
|
||||||
ProviderState.AdditionalBadge.BestTrade
|
ProviderState.AdditionalBadge.BestTrade
|
||||||
} else {
|
} else {
|
||||||
ProviderState.AdditionalBadge.Empty
|
ProviderState.AdditionalBadge.Empty
|
||||||
|
|
@ -1549,12 +1555,12 @@ internal class StateBuilder(
|
||||||
id = this.providerId,
|
id = this.providerId,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
iconUrl = this.imageLarge,
|
iconUrl = this.imageLarge,
|
||||||
type = this.type.toString(),
|
type = this.type.providerName,
|
||||||
subtitle = stringReference(rateString),
|
subtitle = stringReference(rateString),
|
||||||
additionalBadge = badge,
|
additionalBadge = badge,
|
||||||
selectionType = selectionType,
|
selectionType = selectionType,
|
||||||
percentLowerThenBest = PercentDifference.Empty,
|
percentLowerThenBest = PercentDifference.Empty,
|
||||||
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
|
namePrefix = ProviderState.PrefixType.NONE,
|
||||||
onProviderClick = onProviderClick,
|
onProviderClick = onProviderClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1569,6 +1575,8 @@ internal class StateBuilder(
|
||||||
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency)
|
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency)
|
||||||
val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) {
|
val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) {
|
||||||
ProviderState.AdditionalBadge.PermissionRequired
|
ProviderState.AdditionalBadge.PermissionRequired
|
||||||
|
} else if (isRecommended) {
|
||||||
|
ProviderState.AdditionalBadge.Recommended
|
||||||
} else {
|
} else {
|
||||||
ProviderState.AdditionalBadge.Empty
|
ProviderState.AdditionalBadge.Empty
|
||||||
}
|
}
|
||||||
|
|
@ -1576,7 +1584,7 @@ internal class StateBuilder(
|
||||||
id = this.providerId,
|
id = this.providerId,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
iconUrl = this.imageLarge,
|
iconUrl = this.imageLarge,
|
||||||
type = this.type.toString(),
|
type = this.type.providerName,
|
||||||
subtitle = stringReference(rateString),
|
subtitle = stringReference(rateString),
|
||||||
additionalBadge = additionalBadge,
|
additionalBadge = additionalBadge,
|
||||||
selectionType = selectionType,
|
selectionType = selectionType,
|
||||||
|
|
@ -1605,24 +1613,26 @@ internal class StateBuilder(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SwapProvider.convertToAvailableFromProviderState(
|
private fun SwapProvider.convertToAvailableFromProviderState(
|
||||||
|
swapProvider: SwapProvider,
|
||||||
alertText: TextReference,
|
alertText: TextReference,
|
||||||
selectionType: ProviderState.SelectionType,
|
selectionType: ProviderState.SelectionType,
|
||||||
onProviderClick: (String) -> Unit,
|
onProviderClick: (String) -> Unit,
|
||||||
): ProviderState {
|
): ProviderState {
|
||||||
|
val additionalBadge = if (swapProvider.isRecommended) {
|
||||||
|
ProviderState.AdditionalBadge.Recommended
|
||||||
|
} else {
|
||||||
|
ProviderState.AdditionalBadge.Empty
|
||||||
|
}
|
||||||
return ProviderState.Content(
|
return ProviderState.Content(
|
||||||
id = this.providerId,
|
id = this.providerId,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
iconUrl = this.imageLarge,
|
iconUrl = this.imageLarge,
|
||||||
type = this.type.toString(),
|
type = this.type.providerName,
|
||||||
selectionType = selectionType,
|
selectionType = selectionType,
|
||||||
subtitle = alertText,
|
subtitle = alertText,
|
||||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
additionalBadge = additionalBadge,
|
||||||
percentLowerThenBest = PercentDifference.Empty,
|
percentLowerThenBest = PercentDifference.Empty,
|
||||||
namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) {
|
namePrefix = ProviderState.PrefixType.NONE,
|
||||||
ProviderState.PrefixType.PROVIDED_BY
|
|
||||||
} else {
|
|
||||||
ProviderState.PrefixType.NONE
|
|
||||||
},
|
|
||||||
onProviderClick = onProviderClick,
|
onProviderClick = onProviderClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ private val state = SwapSuccessStateHolder(
|
||||||
txUrl = "https://www.google.com/#q=nam",
|
txUrl = "https://www.google.com/#q=nam",
|
||||||
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
||||||
providerName = TextReference.Str("1inch"),
|
providerName = TextReference.Str("1inch"),
|
||||||
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
|
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
|
||||||
showStatusButton = false,
|
showStatusButton = false,
|
||||||
providerIcon = "",
|
providerIcon = "",
|
||||||
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,10 @@ private fun Content(
|
||||||
|
|
||||||
if (amountEquivalent != null) {
|
if (amountEquivalent != null) {
|
||||||
if (type is TransactionCardType.ReadOnly) {
|
if (type is TransactionCardType.ReadOnly) {
|
||||||
Row(modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20)) {
|
Row(
|
||||||
|
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
if (priceImpact is PriceImpact.Value) {
|
if (priceImpact is PriceImpact.Value) {
|
||||||
Text(
|
Text(
|
||||||
text = makePriceImpactBalanceWarning(
|
text = makePriceImpactBalanceWarning(
|
||||||
|
|
@ -291,8 +294,6 @@ private fun Content(
|
||||||
),
|
),
|
||||||
color = TangemTheme.colors.text.tertiary,
|
color = TangemTheme.colors.text.tertiary,
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterVertically),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
AnimatedContent(targetState = amountEquivalent, label = "") {
|
AnimatedContent(targetState = amountEquivalent, label = "") {
|
||||||
|
|
@ -312,7 +313,7 @@ private fun Content(
|
||||||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_alert_24),
|
painter = painterResource(id = R.drawable.ic_information_24),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (priceImpact is PriceImpact.Value) {
|
tint = if (priceImpact is PriceImpact.Value) {
|
||||||
TangemTheme.colors.text.attention
|
TangemTheme.colors.text.attention
|
||||||
|
|
|
||||||
|
|
@ -435,8 +435,11 @@ internal class SwapViewModel @Inject constructor(
|
||||||
if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) {
|
if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) {
|
||||||
currentSelected
|
currentSelected
|
||||||
} else {
|
} else {
|
||||||
findBestQuoteProvider(consideredProviders.getLastLoadedSuccessStates())
|
val successLoadedData = consideredProviders.getLastLoadedSuccessStates()
|
||||||
?: consideredProviders.keys.first()
|
val recommendedProvider = successLoadedData.keys.firstOrNull { it.isRecommended }
|
||||||
|
val bestQuotesProvider = findBestQuoteProvider(successLoadedData)
|
||||||
|
triggerPromoProviderEvent(recommendedProvider, bestQuotesProvider)
|
||||||
|
recommendedProvider ?: bestQuotesProvider ?: consideredProviders.keys.first()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.keys.first()
|
state.keys.first()
|
||||||
|
|
@ -1181,11 +1184,28 @@ internal class SwapViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun triggerPromoProviderEvent(recommendedProvider: SwapProvider?, bestQuotesProvider: SwapProvider?) {
|
||||||
|
// for now send event only for changelly
|
||||||
|
if (recommendedProvider == null ||
|
||||||
|
recommendedProvider.providerId != CHANGELLY_PROVIDER_ID ||
|
||||||
|
bestQuotesProvider == null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val event = if (recommendedProvider.providerId == bestQuotesProvider.providerId) {
|
||||||
|
SwapEvents.ChangellyActivity(SwapEvents.ChangellyActivity.PromoState.Native)
|
||||||
|
} else {
|
||||||
|
SwapEvents.ChangellyActivity(SwapEvents.ChangellyActivity.PromoState.Recommended)
|
||||||
|
}
|
||||||
|
analyticsEventHandler.send(event = event)
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val loggingTag = "SwapViewModel"
|
const val loggingTag = "SwapViewModel"
|
||||||
const val INITIAL_AMOUNT = ""
|
const val INITIAL_AMOUNT = ""
|
||||||
const val UPDATE_DELAY = 10000L
|
const val UPDATE_DELAY = 10000L
|
||||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||||
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||||
|
const val CHANGELLY_PROVIDER_ID = "changelly"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.analytics
|
||||||
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
|
||||||
|
|
||||||
|
|
@ -35,12 +37,15 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
||||||
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
||||||
currency = cryptoCurrency,
|
currency = cryptoCurrency,
|
||||||
)
|
)
|
||||||
|
is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||||
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||||
|
source = AnalyticsParam.ScreensSources.Token,
|
||||||
|
)
|
||||||
is TokenDetailsNotification.NetworksUnreachable,
|
is TokenDetailsNotification.NetworksUnreachable,
|
||||||
is TokenDetailsNotification.ExistentialDeposit,
|
is TokenDetailsNotification.ExistentialDeposit,
|
||||||
is TokenDetailsNotification.NetworksNoAccount,
|
is TokenDetailsNotification.NetworksNoAccount,
|
||||||
is TokenDetailsNotification.TopUpWithoutReserve,
|
is TokenDetailsNotification.TopUpWithoutReserve,
|
||||||
is TokenDetailsNotification.RentInfo,
|
is TokenDetailsNotification.RentInfo,
|
||||||
is TokenDetailsNotification.SwapPromo,
|
|
||||||
is TokenDetailsNotification.NetworkShutdown,
|
is TokenDetailsNotification.NetworkShutdown,
|
||||||
is TokenDetailsNotification.HederaAssociateWarning,
|
is TokenDetailsNotification.HederaAssociateWarning,
|
||||||
is TokenDetailsNotification.KoinosMana,
|
is TokenDetailsNotification.KoinosMana,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
import com.tangem.core.ui.extensions.*
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.networkIconResId
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||||
import com.tangem.features.tokendetails.impl.R
|
import com.tangem.features.tokendetails.impl.R
|
||||||
|
|
@ -45,16 +48,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
||||||
val onCloseClick: () -> Unit,
|
val onCloseClick: () -> Unit,
|
||||||
) : TokenDetailsNotification(
|
) : TokenDetailsNotification(
|
||||||
config = NotificationConfig(
|
config = NotificationConfig(
|
||||||
title = resourceReference(
|
title = resourceReference(id = R.string.swap_promo_title),
|
||||||
id = R.string.token_swap_changelly_promotion_title,
|
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||||
formatArgs = wrappedList("0%"),
|
iconResId = R.drawable.img_okx_dex_logo,
|
||||||
),
|
|
||||||
subtitle = resourceReference(
|
|
||||||
id = R.string.token_swap_changelly_promotion_message,
|
|
||||||
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
|
|
||||||
),
|
|
||||||
iconResId = R.drawable.img_swap_promo,
|
|
||||||
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
|
|
||||||
onCloseClick = onCloseClick,
|
onCloseClick = onCloseClick,
|
||||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||||
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),
|
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
||||||
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList<ExchangeStatusState> {
|
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList<ExchangeStatusState> {
|
||||||
if (status == null) return persistentListOf()
|
if (status == null) return persistentListOf()
|
||||||
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
||||||
|
val isWaitingTxHash = status == ExchangeStatus.WaitingTxHash
|
||||||
val isConfirming = status == ExchangeStatus.Confirming
|
val isConfirming = status == ExchangeStatus.Confirming
|
||||||
val isVerifying = status == ExchangeStatus.Verifying
|
val isVerifying = status == ExchangeStatus.Verifying
|
||||||
val isExchanging = status == ExchangeStatus.Exchanging
|
val isExchanging = status == ExchangeStatus.Exchanging
|
||||||
|
|
@ -174,27 +175,35 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
||||||
val isExchangingDone = !isExchanging && isConfirmingDone
|
val isExchangingDone = !isExchanging && isConfirmingDone
|
||||||
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
|
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
|
||||||
|
|
||||||
return if (status == ExchangeStatus.Cancelled) {
|
return buildList {
|
||||||
listOf(cancelledStep())
|
when {
|
||||||
} else {
|
status == ExchangeStatus.Cancelled -> add(cancelledStep())
|
||||||
listOf(
|
isWaitingTxHash -> add(waitTxStep())
|
||||||
waitStep(isWaiting, isWaitingDone),
|
// ExchangeStatus.Unknown is temporary added for 1inch
|
||||||
confirmStep(isConfirming, isConfirmingDone),
|
status == ExchangeStatus.Unknown -> add(unknownStateStep())
|
||||||
exchangeStep(
|
else -> {
|
||||||
isExchanging = isExchanging,
|
add(waitStep(isWaiting, isWaitingDone))
|
||||||
isExchangingDone = isExchangingDone,
|
add(confirmStep(isConfirming, isConfirmingDone))
|
||||||
isRefunded = isRefunded,
|
add(
|
||||||
hasFailed = hasFailed,
|
exchangeStep(
|
||||||
isVerifying = isVerifying,
|
isExchanging = isExchanging,
|
||||||
isFailed = isFailed,
|
isExchangingDone = isExchangingDone,
|
||||||
),
|
isRefunded = isRefunded,
|
||||||
sendStep(
|
hasFailed = hasFailed,
|
||||||
isSending = isSending,
|
isVerifying = isVerifying,
|
||||||
isSendingDone = isSendingDone,
|
isFailed = isFailed,
|
||||||
isRefunded = isRefunded,
|
),
|
||||||
hasFailed = hasFailed,
|
)
|
||||||
),
|
add(
|
||||||
)
|
sendStep(
|
||||||
|
isSending = isSending,
|
||||||
|
isSendingDone = isSendingDone,
|
||||||
|
isRefunded = isRefunded,
|
||||||
|
hasFailed = hasFailed,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}.toPersistentList()
|
}.toPersistentList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,6 +225,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
||||||
isDone = isNewDone,
|
isDone = isNewDone,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun waitTxStep() = ExchangeStatusState(
|
||||||
|
status = ExchangeStatus.Verifying,
|
||||||
|
text = TextReference.Res(R.string.express_exchange_status_waiting_tx_hash),
|
||||||
|
isActive = false,
|
||||||
|
isDone = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun unknownStateStep() = ExchangeStatusState(
|
||||||
|
status = ExchangeStatus.Failed,
|
||||||
|
text = TextReference.Res(R.string.express_exchange_status_failed),
|
||||||
|
isActive = false,
|
||||||
|
isDone = true,
|
||||||
|
)
|
||||||
|
|
||||||
private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
|
private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
|
||||||
status = ExchangeStatus.Confirming,
|
status = ExchangeStatus.Confirming,
|
||||||
text = when {
|
text = when {
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
|
||||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
|
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
|
||||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||||
import com.tangem.core.ui.components.notifications.Notification
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
import com.tangem.core.ui.components.notifications.OkxPromoNotification
|
||||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||||
import com.tangem.core.ui.components.transactions.txHistoryItems
|
import com.tangem.core.ui.components.transactions.txHistoryItems
|
||||||
import com.tangem.core.ui.event.EventEffect
|
import com.tangem.core.ui.event.EventEffect
|
||||||
|
|
@ -116,7 +116,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
||||||
contentType = { it.config::class.java },
|
contentType = { it.config::class.java },
|
||||||
itemContent = {
|
itemContent = {
|
||||||
if (it is TokenDetailsNotification.SwapPromo) {
|
if (it is TokenDetailsNotification.SwapPromo) {
|
||||||
NotificationWithBackground(
|
OkxPromoNotification(
|
||||||
config = it.config,
|
config = it.config,
|
||||||
modifier = itemModifier.animateItemPlacement(),
|
modifier = itemModifier.animateItemPlacement(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
||||||
SpacerH12()
|
SpacerH12()
|
||||||
ExchangeProvider(
|
ExchangeProvider(
|
||||||
providerName = TextReference.Str(config.provider.name),
|
providerName = TextReference.Str(config.provider.name),
|
||||||
providerType = TextReference.Str(config.provider.type.name),
|
providerType = TextReference.Str(config.provider.type.providerName),
|
||||||
providerTxId = config.txExternalId,
|
providerTxId = config.txExternalId,
|
||||||
imageUrl = config.provider.imageLarge,
|
imageUrl = config.provider.imageLarge,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,7 @@ internal class ExchangeStatusFactory(
|
||||||
return when (status) {
|
return when (status) {
|
||||||
ExchangeStatus.New,
|
ExchangeStatus.New,
|
||||||
ExchangeStatus.Waiting,
|
ExchangeStatus.Waiting,
|
||||||
|
ExchangeStatus.WaitingTxHash,
|
||||||
ExchangeStatus.Sending,
|
ExchangeStatus.Sending,
|
||||||
ExchangeStatus.Confirming,
|
ExchangeStatus.Confirming,
|
||||||
ExchangeStatus.Exchanging,
|
ExchangeStatus.Exchanging,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.blockchain.common.address.AddressType
|
||||||
import com.tangem.common.routing.AppRoute
|
import com.tangem.common.routing.AppRoute
|
||||||
import com.tangem.common.routing.bundle.unbundle
|
import com.tangem.common.routing.bundle.unbundle
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.core.deeplink.DeepLinksRegistry
|
import com.tangem.core.deeplink.DeepLinksRegistry
|
||||||
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
|
||||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||||
|
|
@ -565,7 +566,13 @@ internal class TokenDetailsViewModel @Inject constructor(
|
||||||
ifRight = { it },
|
ifRight = { it },
|
||||||
)
|
)
|
||||||
if (extendedKey.isNotBlank()) {
|
if (extendedKey.isNotBlank()) {
|
||||||
router.share(extendedKey)
|
hapticManager.vibrateMeduim()
|
||||||
|
clipboardManager.setText(text = extendedKey)
|
||||||
|
internalUiState.value = stateFactory.getStateAndTriggerEvent(
|
||||||
|
state = internalUiState.value,
|
||||||
|
errorMessage = resourceReference(R.string.wallet_notification_address_copied),
|
||||||
|
setUiState = { internalUiState.value = it },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -737,14 +744,26 @@ internal class TokenDetailsViewModel @Inject constructor(
|
||||||
override fun onSwapPromoDismiss() {
|
override fun onSwapPromoDismiss() {
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
shouldShowSwapPromoTokenUseCase.neverToShow()
|
shouldShowSwapPromoTokenUseCase.neverToShow()
|
||||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close)
|
analyticsEventsHandler.send(
|
||||||
|
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||||
|
source = AnalyticsParam.ScreensSources.Token,
|
||||||
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||||
|
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSwapPromoClick() {
|
override fun onSwapPromoClick() {
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
shouldShowSwapPromoTokenUseCase.neverToShow()
|
shouldShowSwapPromoTokenUseCase.neverToShow()
|
||||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol))
|
analyticsEventsHandler.send(
|
||||||
|
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||||
|
source = AnalyticsParam.ScreensSources.Token,
|
||||||
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||||
|
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onSwapClick(ScenarioUnavailabilityReason.None)
|
onSwapClick(ScenarioUnavailabilityReason.None)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent {
|
||||||
|
|
||||||
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Promotion(
|
|
||||||
event: String,
|
|
||||||
params: Map<String, String> = mapOf(),
|
|
||||||
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
|
||||||
class NoticePromotionBanner(
|
|
||||||
source: AnalyticsParam.ScreensSources,
|
|
||||||
programName: String,
|
|
||||||
) : Promotion(
|
|
||||||
event = "Notice - Promotion Banner",
|
|
||||||
params = mapOf(
|
|
||||||
AnalyticsParam.SOURCE to source.value,
|
|
||||||
"Program Name" to programName,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class PromotionBannerClicked(
|
|
||||||
source: AnalyticsParam.ScreensSources,
|
|
||||||
programName: String,
|
|
||||||
action: BannerAction,
|
|
||||||
) : Promotion(
|
|
||||||
event = "Promo Banner Clicked",
|
|
||||||
params = mapOf(
|
|
||||||
AnalyticsParam.SOURCE to source.value,
|
|
||||||
"Program Name" to programName,
|
|
||||||
"Action" to action.action,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
sealed class BannerAction(val action: String) {
|
|
||||||
data object Clicked : BannerAction(action = "Clicked")
|
|
||||||
data object Closed : BannerAction(action = "Closed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
import com.tangem.core.analytics.models.AnalyticsParam
|
import com.tangem.core.analytics.models.AnalyticsParam
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||||
|
|
@ -46,16 +46,19 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
|
||||||
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
|
||||||
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||||
is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner(
|
is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||||
source = AnalyticsParam.ScreensSources.Main,
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
programName = "Travala",
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||||
|
)
|
||||||
|
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||||
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||||
)
|
)
|
||||||
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
|
||||||
is WalletNotification.Informational.NoAccount,
|
is WalletNotification.Informational.NoAccount,
|
||||||
is WalletNotification.Warning.LowSignatures,
|
is WalletNotification.Warning.LowSignatures,
|
||||||
is WalletNotification.Warning.SomeNetworksUnreachable,
|
is WalletNotification.Warning.SomeNetworksUnreachable,
|
||||||
is WalletNotification.Warning.NetworksUnreachable,
|
is WalletNotification.Warning.NetworksUnreachable,
|
||||||
is WalletNotification.SwapPromo,
|
|
||||||
-> null
|
-> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||||
import com.tangem.domain.promo.PromoBanner
|
import com.tangem.domain.promo.PromoBanner
|
||||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||||
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||||
import com.tangem.domain.tokens.GetTokenListUseCase
|
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||||
import com.tangem.domain.tokens.error.TokenListError
|
import com.tangem.domain.tokens.error.TokenListError
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
|
@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
private val getTokenListUseCase: GetTokenListUseCase,
|
private val getTokenListUseCase: GetTokenListUseCase,
|
||||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||||
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
|
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||||
private val promoRepository: PromoRepository,
|
private val promoRepository: PromoRepository,
|
||||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||||
private val backupValidator: BackupValidator,
|
private val backupValidator: BackupValidator,
|
||||||
|
|
@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||||
|
|
||||||
val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) }
|
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
|
||||||
return combine(
|
return combine(
|
||||||
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
||||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||||
flow4 = shouldShowTravalaPromoWalletUseCase().conflate(),
|
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
|
||||||
flow5 = travalaPromoFlow.conflate(),
|
flow5 = promoFlow.conflate(),
|
||||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner ->
|
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
|
||||||
|
|
||||||
readyForRateAppNotification = true
|
readyForRateAppNotification = true
|
||||||
buildList {
|
buildList {
|
||||||
addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents)
|
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||||
|
|
||||||
addCriticalNotifications(userWallet)
|
addCriticalNotifications(userWallet)
|
||||||
|
|
||||||
|
|
@ -69,18 +69,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<WalletNotification>.addTravalaPromoNotification(
|
private fun MutableList<WalletNotification>.addSwapPromoNotification(
|
||||||
shouldShowPromo: Boolean,
|
shouldShowPromo: Boolean,
|
||||||
promoBanner: PromoBanner?,
|
promoBanner: PromoBanner?,
|
||||||
clickIntents: WalletClickIntents,
|
clickIntents: WalletClickIntents,
|
||||||
) {
|
) {
|
||||||
promoBanner ?: return
|
promoBanner ?: return
|
||||||
val promoNotification = WalletNotification.TravalaPromo(
|
val promoNotification = WalletNotification.SwapPromo(
|
||||||
startDateTime = promoBanner.bannerState.timeline.start,
|
startDateTime = promoBanner.bannerState.timeline.start,
|
||||||
endDateTime = promoBanner.bannerState.timeline.end,
|
endDateTime = promoBanner.bannerState.timeline.end,
|
||||||
bannerLink = promoBanner.bannerState.link,
|
onCloseClick = clickIntents::onCloseSwapPromoClick,
|
||||||
onBookNowButtonClick = clickIntents::onTravalaPromoClick,
|
|
||||||
onCloseClick = clickIntents::onCloseTravalaPromoClick,
|
|
||||||
)
|
)
|
||||||
addIf(
|
addIf(
|
||||||
element = promoNotification,
|
element = promoNotification,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ fun UserWallet.getCardsCount(): Int? {
|
||||||
return if (isMultiCurrency) {
|
return if (isMultiCurrency) {
|
||||||
when (val status = scanResponse.card.backupStatus) {
|
when (val status = scanResponse.card.backupStatus) {
|
||||||
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
||||||
is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1
|
is CardDTO.BackupStatus.NoBackup,
|
||||||
is CardDTO.BackupStatus.NoBackup -> 1
|
is CardDTO.BackupStatus.CardLinked,
|
||||||
|
-> 1
|
||||||
null -> 1 // Multi-currency wallet without backup function. Example, 4.12
|
null -> 1 // Multi-currency wallet without backup function. Example, 4.12
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ internal object WalletImageResolver {
|
||||||
cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet()
|
cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet()
|
||||||
cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet()
|
cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet()
|
||||||
cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet()
|
cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet()
|
||||||
|
cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet()
|
||||||
|
cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet()
|
||||||
cardTypesResolver.isWallet2() -> userWallet.resolveWallet2()
|
cardTypesResolver.isWallet2() -> userWallet.resolveWallet2()
|
||||||
cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet()
|
cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet()
|
||||||
cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1()
|
cardTypesResolver.isTangemWallet() -> userWallet.resolveWallet1()
|
||||||
|
|
@ -227,6 +229,22 @@ internal object WalletImageResolver {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun UserWallet.resolveVividWallet(): Int? {
|
||||||
|
// for multicolored cards use image of 3 cards in all cases
|
||||||
|
return resolveWallet2(
|
||||||
|
oneBackupResId = R.drawable.ill_vivid_cards3_120_106,
|
||||||
|
twoBackupResId = R.drawable.ill_vivid_cards3_120_106,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun UserWallet.resolvePastelWallet(): Int? {
|
||||||
|
// for multicolored cards use image of 3 cards in all cases
|
||||||
|
return resolveWallet2(
|
||||||
|
oneBackupResId = R.drawable.ill_pastel_cards3_120_106,
|
||||||
|
twoBackupResId = R.drawable.ill_pastel_cards3_120_106,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun UserWallet.resolveWallet1(): Int? {
|
private fun UserWallet.resolveWallet1(): Int? {
|
||||||
return resolveWalletWithBackups { count ->
|
return resolveWalletWithBackups { count ->
|
||||||
when (count) {
|
when (count) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ internal class WalletStateController @Inject constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(transformer: WalletScreenStateTransformer) {
|
fun update(transformer: WalletScreenStateTransformer) {
|
||||||
|
Timber.d("Applying: ${transformer::class.simpleName}")
|
||||||
mutableUiState.update(function = transformer::transform)
|
mutableUiState.update(function = transformer::transform)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||||
import com.tangem.core.ui.extensions.*
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.pluralReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
@ -205,16 +208,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
||||||
val onCloseClick: () -> Unit,
|
val onCloseClick: () -> Unit,
|
||||||
) : WalletNotification(
|
) : WalletNotification(
|
||||||
config = NotificationConfig(
|
config = NotificationConfig(
|
||||||
title = resourceReference(
|
title = resourceReference(id = R.string.swap_promo_title),
|
||||||
id = R.string.main_swap_changelly_promotion_title,
|
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||||
formatArgs = wrappedList("0%"),
|
iconResId = R.drawable.img_okx_dex_logo,
|
||||||
),
|
|
||||||
subtitle = resourceReference(
|
|
||||||
id = R.string.main_swap_changelly_promotion_message,
|
|
||||||
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
|
|
||||||
),
|
|
||||||
iconResId = R.drawable.img_swap_promo,
|
|
||||||
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
|
|
||||||
onCloseClick = onCloseClick,
|
onCloseClick = onCloseClick,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,32 @@
|
||||||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||||
|
|
||||||
|
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.tokens.error.TokenListError
|
import com.tangem.domain.tokens.error.TokenListError
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||||
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
internal class SetTokenListErrorTransformer(
|
internal class SetTokenListErrorTransformer(
|
||||||
userWalletId: UserWalletId,
|
private val selectedWallet: UserWallet,
|
||||||
private val error: TokenListError,
|
private val error: TokenListError,
|
||||||
) : WalletStateTransformer(userWalletId) {
|
private val appCurrency: AppCurrency,
|
||||||
|
) : WalletStateTransformer(selectedWallet.walletId) {
|
||||||
|
|
||||||
override fun transform(prevState: WalletState): WalletState {
|
override fun transform(prevState: WalletState): WalletState {
|
||||||
return when (error) {
|
return when (error) {
|
||||||
is TokenListError.EmptyTokens -> {
|
is TokenListError.EmptyTokens -> {
|
||||||
when (prevState) {
|
when (prevState) {
|
||||||
is WalletState.MultiCurrency.Content -> {
|
is WalletState.MultiCurrency.Content -> {
|
||||||
prevState.copy(tokensListState = WalletTokensListState.Empty)
|
prevState.copy(
|
||||||
|
walletCardState = prevState.walletCardState.toLoadedState(),
|
||||||
|
tokensListState = WalletTokensListState.Empty,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is WalletState.MultiCurrency.Locked -> {
|
is WalletState.MultiCurrency.Locked -> {
|
||||||
Timber.w("Impossible to load tokens list for locked wallet")
|
Timber.w("Impossible to load tokens list for locked wallet")
|
||||||
|
|
@ -37,4 +47,21 @@ internal class SetTokenListErrorTransformer(
|
||||||
-> prevState
|
-> prevState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun WalletCardState.toLoadedState(): WalletCardState {
|
||||||
|
return WalletCardState.Content(
|
||||||
|
id = id,
|
||||||
|
title = title,
|
||||||
|
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
|
||||||
|
imageResId = imageResId,
|
||||||
|
onRenameClick = onRenameClick,
|
||||||
|
onDeleteClick = onDeleteClick,
|
||||||
|
balance = BigDecimalFormatter.formatFiatAmount(
|
||||||
|
fiatAmount = BigDecimal.ZERO,
|
||||||
|
fiatCurrencyCode = appCurrency.code,
|
||||||
|
fiatCurrencySymbol = appCurrency.symbol,
|
||||||
|
),
|
||||||
|
cardCount = selectedWallet.getCardsCount(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +61,11 @@ internal abstract class BasicTokenListSubscriber(
|
||||||
},
|
},
|
||||||
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
|
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
|
||||||
transform = { maybeTokenList, maybeAppCurrency ->
|
transform = { maybeTokenList, maybeAppCurrency ->
|
||||||
|
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
||||||
|
Timber.e("Failed to load app currency: $e")
|
||||||
|
AppCurrency.Default
|
||||||
|
}
|
||||||
|
|
||||||
val tokenList = maybeTokenList.getOrElse(
|
val tokenList = maybeTokenList.getOrElse(
|
||||||
ifLoading = { maybeContent ->
|
ifLoading = { maybeContent ->
|
||||||
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
|
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
|
||||||
|
|
@ -74,16 +79,17 @@ internal abstract class BasicTokenListSubscriber(
|
||||||
},
|
},
|
||||||
ifError = { e ->
|
ifError = { e ->
|
||||||
Timber.e("Failed to load token list: $e")
|
Timber.e("Failed to load token list: $e")
|
||||||
SetTokenListErrorTransformer(userWallet.walletId, e)
|
stateHolder.update(
|
||||||
|
SetTokenListErrorTransformer(
|
||||||
|
selectedWallet = userWallet,
|
||||||
|
error = e,
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
),
|
||||||
|
)
|
||||||
return@combine
|
return@combine
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
|
||||||
Timber.e("Failed to load app currency: $e")
|
|
||||||
AppCurrency.Default
|
|
||||||
}
|
|
||||||
|
|
||||||
updateContent(tokenList, appCurrency)
|
updateContent(tokenList, appCurrency)
|
||||||
walletWithFundsChecker.check(tokenList)
|
walletWithFundsChecker.check(tokenList)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.tangem.core.ui.components.notifications.Notification
|
import com.tangem.core.ui.components.notifications.Notification
|
||||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
import com.tangem.core.ui.components.notifications.OkxPromoNotification
|
||||||
import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground
|
import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
|
||||||
|
|
@ -29,7 +29,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
||||||
// TODO develop promo banner general component
|
// TODO develop promo banner general component
|
||||||
when (it) {
|
when (it) {
|
||||||
is WalletNotification.SwapPromo -> {
|
is WalletNotification.SwapPromo -> {
|
||||||
NotificationWithBackground(
|
OkxPromoNotification(
|
||||||
config = it.config,
|
config = it.config,
|
||||||
modifier = modifier.animateItemPlacement(),
|
modifier = modifier.animateItemPlacement(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import arrow.core.getOrElse
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.navigation.settings.SettingsManager
|
import com.tangem.core.navigation.settings.SettingsManager
|
||||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||||
|
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
|
||||||
import com.tangem.domain.settings.*
|
import com.tangem.domain.settings.*
|
||||||
import com.tangem.domain.wallets.models.UserWalletId
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||||
|
|
@ -21,7 +22,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
|
import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig
|
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
|
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
|
||||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||||
|
|
@ -61,6 +61,7 @@ internal class WalletViewModel @Inject constructor(
|
||||||
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
|
private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender,
|
||||||
private val walletDeepLinksHandler: WalletDeepLinksHandler,
|
private val walletDeepLinksHandler: WalletDeepLinksHandler,
|
||||||
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
|
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
|
||||||
|
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
|
||||||
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
||||||
private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase,
|
private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase,
|
||||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||||
|
|
@ -206,7 +207,7 @@ internal class WalletViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to update the current wallet if the application was in the background for more than 10 seconds
|
// We need to update the current wallet quotes if the application was in the background for more than 10 seconds
|
||||||
// and then returned to the foreground
|
// and then returned to the foreground
|
||||||
private fun subscribeToScreenBackgroundState() {
|
private fun subscribeToScreenBackgroundState() {
|
||||||
screenLifecycleProvider.isBackgroundState
|
screenLifecycleProvider.isBackgroundState
|
||||||
|
|
@ -214,7 +215,7 @@ internal class WalletViewModel @Inject constructor(
|
||||||
refreshWalletJobHolder.cancel()
|
refreshWalletJobHolder.cancel()
|
||||||
when {
|
when {
|
||||||
isBackground -> needToRefreshTimer()
|
isBackground -> needToRefreshTimer()
|
||||||
needToRefreshWallet && !isBackground -> triggerRefreshWallet()
|
needToRefreshWallet && !isBackground -> triggerRefreshWalletQuotes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
@ -227,13 +228,15 @@ internal class WalletViewModel @Inject constructor(
|
||||||
}.saveIn(refreshWalletJobHolder)
|
}.saveIn(refreshWalletJobHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun triggerRefreshWallet() {
|
private fun triggerRefreshWalletQuotes() {
|
||||||
needToRefreshWallet = false
|
needToRefreshWallet = false
|
||||||
val state = stateHolder.uiState.value
|
val state = stateHolder.uiState.value
|
||||||
val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return
|
val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return
|
||||||
wallet.pullToRefreshConfig.onRefresh.invoke(
|
viewModelScope.launch {
|
||||||
WalletPullToRefreshConfig.ShowRefreshState(false),
|
refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse {
|
||||||
)
|
Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it")
|
||||||
|
}
|
||||||
|
}.saveIn(refreshWalletJobHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) {
|
private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) {
|
||||||
|
|
@ -244,6 +247,14 @@ internal class WalletViewModel @Inject constructor(
|
||||||
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
|
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
|
||||||
is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action)
|
is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action)
|
||||||
is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> {
|
is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> {
|
||||||
|
// refresh loader to use actual user wallet
|
||||||
|
walletScreenContentLoader.load(
|
||||||
|
userWallet = action.selectedWallet,
|
||||||
|
clickIntents = clickIntents,
|
||||||
|
isRefresh = true,
|
||||||
|
coroutineScope = viewModelScope,
|
||||||
|
)
|
||||||
|
|
||||||
stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet))
|
stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet))
|
||||||
}
|
}
|
||||||
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||||
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
|
import com.tangem.domain.appcurrency.extenstions.unwrap
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
|
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
|
||||||
import com.tangem.domain.tokens.FetchCardTokenListUseCase
|
import com.tangem.domain.tokens.FetchCardTokenListUseCase
|
||||||
|
|
@ -38,6 +40,7 @@ internal class WalletClickIntents @Inject constructor(
|
||||||
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
||||||
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
|
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
|
||||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||||
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
|
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
|
@ -118,7 +121,13 @@ internal class WalletClickIntents @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeFetchResult.onLeft {
|
maybeFetchResult.onLeft {
|
||||||
stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it))
|
stateHolder.update(
|
||||||
|
SetTokenListErrorTransformer(
|
||||||
|
selectedWallet = userWallet,
|
||||||
|
error = it,
|
||||||
|
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
stateHolder.update(
|
stateHolder.update(
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||||
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
||||||
import com.tangem.domain.tokens.FetchTokenListUseCase
|
import com.tangem.domain.tokens.FetchTokenListUseCase
|
||||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||||
|
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
|
||||||
import com.tangem.domain.wallets.models.UnlockWalletsError
|
import com.tangem.domain.wallets.models.UnlockWalletsError
|
||||||
import com.tangem.domain.wallets.models.UserWallet
|
import com.tangem.domain.wallets.models.UserWallet
|
||||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||||
import com.tangem.feature.wallet.impl.R
|
import com.tangem.feature.wallet.impl.R
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
||||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
|
||||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
||||||
|
|
@ -222,6 +222,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCloseSwapPromoClick() {
|
override fun onCloseSwapPromoClick() {
|
||||||
|
analyticsEventHandler.send(
|
||||||
|
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||||
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||||
|
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||||
|
),
|
||||||
|
)
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
shouldShowSwapPromoWalletUseCase.neverToShow()
|
shouldShowSwapPromoWalletUseCase.neverToShow()
|
||||||
}
|
}
|
||||||
|
|
@ -229,10 +236,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
|
|
||||||
override fun onTravalaPromoClick(link: String?) {
|
override fun onTravalaPromoClick(link: String?) {
|
||||||
analyticsEventHandler.send(
|
analyticsEventHandler.send(
|
||||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||||
source = AnalyticsParam.ScreensSources.Main,
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
programName = "Travala",
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked,
|
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
link?.let {
|
link?.let {
|
||||||
|
|
@ -244,10 +251,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
||||||
|
|
||||||
override fun onCloseTravalaPromoClick() {
|
override fun onCloseTravalaPromoClick() {
|
||||||
analyticsEventHandler.send(
|
analyticsEventHandler.send(
|
||||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||||
source = AnalyticsParam.ScreensSources.Main,
|
source = AnalyticsParam.ScreensSources.Main,
|
||||||
programName = "Travala",
|
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed,
|
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
viewModelScope.launch(dispatchers.main) {
|
viewModelScope.launch(dispatchers.main) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
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