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
|
||||
android:name="com.tangem.tap.MainActivity"
|
||||
android:configChanges="uiMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait"
|
||||
|
|
|
|||
|
|
@ -7,16 +7,16 @@ import android.os.Bundle
|
|||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import java.util.*
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ForegroundActivityObserver : ActivityResultCaller {
|
||||
override var activityResultLauncher: ActivityResultLauncher<Intent>? = null
|
||||
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
|
||||
.filterNot { it.value.isDestroyed }
|
||||
.firstOrNull()
|
||||
|
|
@ -35,7 +35,7 @@ class ForegroundActivityObserver : ActivityResultCaller {
|
|||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
activities[activity::class] = activity
|
||||
activities[activity::class] = activity as? AppCompatActivity
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
|||
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.lifecycle.Lifecycle
|
||||
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.Snackbar
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
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.extensions.TextReference
|
||||
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.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
|
|
@ -114,7 +117,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
lateinit var testerRouter: TesterRouter
|
||||
|
||||
@Inject
|
||||
lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver
|
||||
lateinit var cardSdkOwner: CardSdkOwner
|
||||
|
||||
@Inject
|
||||
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
|
||||
|
|
@ -174,6 +177,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
@Inject
|
||||
internal lateinit var routingComponentFactory: RoutingComponent.Factory
|
||||
|
||||
@Inject
|
||||
internal lateinit var appRouter: AppRouter
|
||||
|
||||
@Inject
|
||||
lateinit var pushNotificationsRouter: PushNotificationsRouter
|
||||
|
||||
|
|
@ -182,7 +188,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
||||
// TODO: fixme: inject through DI
|
||||
private val intentProcessor: IntentProcessor = IntentProcessor()
|
||||
|
|
@ -194,9 +200,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
||||
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(
|
||||
navigationBarStyle = SystemBarStyle.auto(
|
||||
|
|
@ -270,7 +277,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
private fun installActivityDependencies() {
|
||||
cardSdkLifecycleObserver.onCreate(context = this)
|
||||
cardSdkOwner.register(activity = this)
|
||||
tangemSdkManager = injectedTangemSdkManager
|
||||
appStateHolder.tangemSdkManager = tangemSdkManager
|
||||
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
|
||||
|
|
@ -301,14 +308,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
private fun installAppTheme() {
|
||||
appThemeModeFlow = createAppThemeModeFlow()
|
||||
val mode = runBlocking { appThemeModeFlow.filterNotNull().first() }
|
||||
val mode = runBlocking { appThemeModeFlow.first() }
|
||||
|
||||
updateAppTheme(mode)
|
||||
}
|
||||
|
||||
private fun observeAppThemeModeUpdates() {
|
||||
appThemeModeFlow
|
||||
.filterNotNull()
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach(::updateAppTheme)
|
||||
.launchIn(lifecycleScope)
|
||||
|
|
@ -324,15 +330,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
|
||||
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
|
||||
val tangemApplication = application as TangemApplication
|
||||
|
||||
return tangemApplication.getAppThemeModeUseCase()
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.map { maybeMode ->
|
||||
maybeMode.getOrElse { AppThemeMode.DEFAULT }
|
||||
}
|
||||
.shareIn(
|
||||
scope = lifecycleScope + Dispatchers.IO,
|
||||
scope = lifecycleScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
|
||||
)
|
||||
}
|
||||
|
|
@ -382,7 +390,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
override fun onDestroy() {
|
||||
intentProcessor.removeAll()
|
||||
cardSdkLifecycleObserver.onDestroy(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
|
@ -393,9 +400,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
private fun updateAppTheme(appThemeMode: AppThemeMode) {
|
||||
MutableAppThemeModeHolder.value = appThemeMode
|
||||
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
|
||||
|
||||
val mode = when (appThemeMode) {
|
||||
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
|
|
@ -403,7 +407,32 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -418,12 +447,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
override fun onNewIntent(intent: 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 {
|
||||
intentProcessor.handleIntent(intent, true)
|
||||
}
|
||||
|
|
@ -505,15 +528,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
}
|
||||
|
||||
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 isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
|
||||
|
||||
when {
|
||||
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||
!isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> {
|
||||
navigateToInitialScreen(intentWhichStartedActivity)
|
||||
}
|
||||
backStackIsEmpty -> {
|
||||
backStack.isEmpty() -> {
|
||||
navigateToInitialScreen(intentWhichStartedActivity)
|
||||
}
|
||||
else -> Unit
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
|
|||
Log.Level.Network,
|
||||
Log.Level.Error,
|
||||
Log.Level.Biometric,
|
||||
Log.Level.Info,
|
||||
)
|
||||
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,11 +36,19 @@ sealed class Settings(
|
|||
) : Settings("Settings / Card Settings", event, params, error) {
|
||||
|
||||
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",
|
||||
params = buildMap {
|
||||
cardsCount?.let { put("Cards Count", "$it") }
|
||||
},
|
||||
error = error,
|
||||
)
|
||||
|
||||
class FactoryResetCanceled(cardsCount: Int) : CardSettings(
|
||||
event = "Factory Reset Canceled",
|
||||
params = mapOf("Cards Count" to "$cardsCount"),
|
||||
)
|
||||
|
||||
class UserCodeChanged : CardSettings("User Code Changed")
|
||||
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.Error,
|
||||
Log.Level.Biometric,
|
||||
Log.Level.Info,
|
||||
)
|
||||
|
||||
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.DefaultCardSdkProvider
|
||||
import com.tangem.tap.data.DefaultCardSdkProvider
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -19,5 +19,5 @@ internal interface CardSdkModule {
|
|||
|
||||
@Binds
|
||||
@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.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CardDomainModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideGetBiometricsStatusUseCase(
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): GetBiometricsStatusUseCase {
|
||||
|
|
@ -30,7 +30,7 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideSetAccessCodeRequestPolicyUseCase(
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): SetAccessCodeRequestPolicyUseCase {
|
||||
|
|
@ -38,35 +38,35 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
|
||||
return WasCardScannedUseCase(cardRepository = cardRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
|
||||
return SetCardWasScannedUseCase(cardRepository = cardRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
|
||||
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
|
||||
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideGetExtendedPublicKeyForCurrencyUseCase(
|
||||
derivationsRepository: DerivationsRepository,
|
||||
): GetExtendedPublicKeyForCurrencyUseCase {
|
||||
|
|
@ -74,13 +74,13 @@ internal object CardDomainModule {
|
|||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
|
||||
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
@Singleton
|
||||
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
|
||||
return DefaultResetCardUseCase(tangemSdkManager)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -398,4 +398,16 @@ internal object TokensDomainModule {
|
|||
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
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.common.*
|
||||
import arrow.fx.coroutines.ResourceScope
|
||||
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.TangemSdkError
|
||||
import com.tangem.common.core.UserCodeRequestPolicy
|
||||
|
|
@ -17,14 +20,14 @@ internal class DefaultResetCardUseCase(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : ResetCardUseCase {
|
||||
|
||||
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = either {
|
||||
enterRequiredAccessCode(card) {
|
||||
override suspend fun invoke(card: CardDTO): Either<ResetCardError, Unit> = resourceScope {
|
||||
either {
|
||||
withUserCodeRequestPolicy(card)
|
||||
|
||||
tangemSdkManager.resetToFactorySettings(
|
||||
cardId = card.cardId,
|
||||
allowsRequestAccessCodeFromRepository = true,
|
||||
)
|
||||
.doOnSuccess { Unit.right() }
|
||||
.doOnFailure { raise(it.mapToDomainError()) }
|
||||
).bind(raise = this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -32,23 +35,29 @@ internal class DefaultResetCardUseCase(
|
|||
cardNumber: Int,
|
||||
card: CardDTO,
|
||||
userWalletId: UserWalletId,
|
||||
): Either<ResetCardError, Unit> = either {
|
||||
enterRequiredAccessCode(card) {
|
||||
tangemSdkManager.resetBackupCard(cardNumber, userWalletId)
|
||||
.doOnSuccess { Unit.right() }
|
||||
.doOnFailure { raise(it.mapToDomainError()) }
|
||||
): Either<ResetCardError, Unit> = resourceScope {
|
||||
either {
|
||||
withUserCodeRequestPolicy(card)
|
||||
|
||||
tangemSdkManager.resetBackupCard(
|
||||
cardNumber = cardNumber,
|
||||
userWalletId = userWalletId,
|
||||
).bind(raise = this)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enterRequiredAccessCode(
|
||||
card: CardDTO,
|
||||
task: suspend TangemSdkManager.() -> CompletionResult<*>,
|
||||
) {
|
||||
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
||||
requestMandatoryAccessCodeEntry(card)
|
||||
private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
|
||||
install(
|
||||
acquire = {
|
||||
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
|
||||
requestMandatoryAccessCodeEntry(card)
|
||||
|
||||
tangemSdkManager.task()
|
||||
.doOnResult { tangemSdkManager.setUserCodeRequestPolicy(policyBeforeReset) }
|
||||
policyBeforeReset
|
||||
},
|
||||
release = { prevPolicy, _ ->
|
||||
tangemSdkManager.setUserCodeRequestPolicy(prevPolicy)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,19 +152,29 @@ internal class BiometricUserWalletsListManager(
|
|||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
||||
changeSelectedUserWalletIdIfNeeded(idsToRemove)
|
||||
if (idsToRemove.size == state.value.userWallets.size) {
|
||||
return clear()
|
||||
}
|
||||
|
||||
return sensitiveInformationRepository.delete(idsToRemove)
|
||||
.flatMap { publicInformationRepository.delete(idsToRemove) }
|
||||
.map { keysRepository.delete(idsToRemove) }
|
||||
.map {
|
||||
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(
|
||||
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
|
||||
userWallets = newUserWallets,
|
||||
isLocked = newUserWallets.any { it.isLocked },
|
||||
userWallets = remainingWallets,
|
||||
isLocked = remainingWallets.any { it.isLocked },
|
||||
selectedUserWalletId = newSelectedUserWallet?.walletId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -173,10 +183,10 @@ internal class BiometricUserWalletsListManager(
|
|||
override suspend fun clear(): CompletionResult<Unit> {
|
||||
return sensitiveInformationRepository.clear()
|
||||
.flatMap { publicInformationRepository.clear() }
|
||||
.map { keysRepository.clear() }
|
||||
.map {
|
||||
keysRepository.clear()
|
||||
selectedUserWalletRepository.set(null)
|
||||
lock()
|
||||
state.value = State()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -301,47 +311,24 @@ internal class BiometricUserWalletsListManager(
|
|||
private fun findOrSetSelectedWallet(
|
||||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
ignorePrevSelectedWallet: Boolean = false,
|
||||
): UserWallet? {
|
||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||
var possibleSelectedUserWallet: UserWallet? = null
|
||||
|
||||
if (!ignorePrevSelectedWallet) {
|
||||
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
|
||||
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
|
||||
}
|
||||
|
||||
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
|
||||
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
|
||||
|
||||
if (possibleSelectedUserWallet != null) {
|
||||
selectedUserWalletRepository.set(possibleSelectedUserWallet.walletId)
|
||||
}
|
||||
}
|
||||
|
||||
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
|
||||
|
||||
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(
|
||||
userWallets: List<UserWallet> = state.value.userWallets,
|
||||
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,
|
||||
|
|
|
|||
|
|
@ -17,5 +17,5 @@ interface CustomTokenInteractor {
|
|||
|
||||
/** Save token [customCurrency] */
|
||||
@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
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.result
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||
|
|
@ -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.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Default implementation of custom token interactor
|
||||
|
|
@ -45,16 +46,18 @@ class DefaultCustomTokenInteractor(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun saveToken(customCurrency: CustomCurrency) {
|
||||
val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it })
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
|
||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
.onRight {
|
||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies)
|
||||
override suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit> {
|
||||
return result {
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
|
||||
error("Failed to get selected wallet: $it")
|
||||
}
|
||||
.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? {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.features.customtoken.impl.presentation
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
|
|
@ -30,11 +32,13 @@ internal class AddCustomTokenFragment : ComposeFragment() {
|
|||
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AddCustomTokenScreen(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.systemBarsPadding(),
|
||||
stateHolder = viewModel.uiState,
|
||||
stateHolder = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,8 +26,8 @@ object ContractAddressValidator {
|
|||
Blockchain.Unknown,
|
||||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
Blockchain.Cardano,
|
||||
-> SuccessAddressValidator.validate(address)
|
||||
Blockchain.Cardano -> blockchain.validateContractAddress(address)
|
||||
else -> blockchain.validateAddress(address)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
|
||||
|
||||
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.KeyboardType
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoTokenAddressConverter
|
||||
import com.tangem.blockchain.blockchains.hedera.HederaTokenAddressConverter
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
|
|
@ -48,6 +46,9 @@ import com.tangem.wallet.R
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.flow.updateAndGet
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -79,12 +80,12 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
private val testActionsHandler = TestActionsHandler()
|
||||
private val formStateBuilder = FormStateBuilder()
|
||||
private val hederaAddressConverter = HederaTokenAddressConverter()
|
||||
private val cardanoTokenAddressConverter = CardanoTokenAddressConverter()
|
||||
|
||||
private var currentCryptoCurrencies: List<CryptoCurrency> = emptyList()
|
||||
|
||||
/** Screen state */
|
||||
var uiState by mutableStateOf(getInitialUiState())
|
||||
private set
|
||||
val uiState: MutableStateFlow<AddCustomTokenStateHolder> = MutableStateFlow(value = getInitialUiState())
|
||||
|
||||
private var foundToken: FoundToken? = null
|
||||
|
||||
|
|
@ -368,48 +369,54 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
viewModelScope.launch(dispatchers.main) {
|
||||
runCatching(dispatchers.io) {
|
||||
val tokenAddress = convertTokenAddress(selectedNetwork, address)
|
||||
?: error("TokenAddress is invalid")
|
||||
|
||||
featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork)
|
||||
}
|
||||
.onSuccess { token ->
|
||||
foundToken = token
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
||||
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
||||
blockchain = Blockchain.fromNetworkId(token.network.id)
|
||||
?: Blockchain.Unknown,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||
isLoading = false,
|
||||
),
|
||||
networkSelectorField = state.form.networkSelectorField.copy(
|
||||
selectedItem = formStateBuilder.createNetworkSelectorItem(
|
||||
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 {
|
||||
foundToken = null
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isLoading = false,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -419,28 +426,31 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isDerivationPathSelected(): Boolean {
|
||||
return with(uiState.form.derivationPathSelectorField?.selectedItem?.blockchain) {
|
||||
this != null && this != Blockchain.Unknown ||
|
||||
uiState.form.derivationPathSelectorField?.selectedItem?.type == DerivationPathSelectorType.CUSTOM &&
|
||||
!uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
}
|
||||
val blockchain = uiState.value.form.derivationPathSelectorField?.selectedItem?.blockchain
|
||||
val selectorType = uiState.value.form.derivationPathSelectorField?.selectedItem?.type
|
||||
|
||||
return blockchain != null && blockchain != Blockchain.Unknown ||
|
||||
selectorType == DerivationPathSelectorType.CUSTOM &&
|
||||
!uiState.value.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
}
|
||||
|
||||
private fun updateWarnings() {
|
||||
uiState = uiState.copySealed(
|
||||
warnings = buildSet {
|
||||
when (getCustomTokenType()) {
|
||||
CustomTokenType.TOKEN -> {
|
||||
addAll(getTokenWarningSet())
|
||||
}
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
warnings = buildSet {
|
||||
when (getCustomTokenType()) {
|
||||
CustomTokenType.TOKEN -> {
|
||||
addAll(getTokenWarningSet())
|
||||
}
|
||||
|
||||
CustomTokenType.BLOCKCHAIN -> {
|
||||
if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken)
|
||||
CustomTokenType.BLOCKCHAIN -> {
|
||||
if (isCustomTokenAlreadyAdded()) add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
if (isDerivationPathSelected()) add(AddCustomTokenWarning.PotentialScamToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCustomTokenType(): CustomTokenType {
|
||||
|
|
@ -452,24 +462,24 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isAnyTokenFieldsFilled(): Boolean {
|
||||
return with(uiState.form) {
|
||||
return with(uiState.value.form) {
|
||||
contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() ||
|
||||
tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAllTokenFieldsFilled(): Boolean {
|
||||
return with(uiState.form) {
|
||||
return with(uiState.value.form) {
|
||||
contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() &&
|
||||
tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTokenWarningSet(): Set<AddCustomTokenWarning> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val networkSelectorValue = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
|
||||
val isContractAddressFieldEmpty = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
address = uiState.value.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
).let {
|
||||
it is ContractAddressValidatorResult.Error && it.type == AddCustomTokenError.FIELD_IS_EMPTY
|
||||
|
|
@ -503,66 +513,70 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun isNetworkSelected(): Boolean {
|
||||
return uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
||||
return uiState.value.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
|
||||
}
|
||||
|
||||
private fun updateFloatingButton() {
|
||||
uiState = updateStateWithDerivationError(uiState)
|
||||
uiState.update { state ->
|
||||
updateStateWithDerivationError(state)
|
||||
}
|
||||
if (isCustomTokenAlreadyAdded()) {
|
||||
uiState = uiState.copySealed(
|
||||
warnings = uiState.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
floatingButton = uiState.floatingButton.copy(isEnabled = false),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
warnings = state.warnings + AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
floatingButton = state.floatingButton.copy(isEnabled = false),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val isCorrectDerivationInput = !uiState.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
val state = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val error = ContractAddressValidator.validate(
|
||||
address = uiState.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
uiState.update { state ->
|
||||
val isCorrectDerivationInput = !state.warnings.contains(AddCustomTokenWarning.WrongDerivationPath)
|
||||
val updatedState = when {
|
||||
isAllTokenFieldsFilled() && isNetworkSelected() -> {
|
||||
val networkSelectorValue = state.form.networkSelectorField.selectedItem.blockchain
|
||||
val error = ContractAddressValidator.validate(
|
||||
address = state.form.contractAddressInputField.value,
|
||||
blockchain = networkSelectorValue,
|
||||
)
|
||||
|
||||
val isSupportedToken = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
it.scanResponse.card.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
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
|
||||
val isSupportedToken = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { false },
|
||||
ifRight = {
|
||||
it.scanResponse.card.canHandleToken(
|
||||
blockchain = networkSelectorValue,
|
||||
cardTypesResolver = it.scanResponse.cardTypesResolver,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
uiState = state.copySealed(
|
||||
warnings = state.warnings - AddCustomTokenWarning.TokenAlreadyAdded,
|
||||
)
|
||||
state.copySealed(
|
||||
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 {
|
||||
|
|
@ -595,13 +609,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
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
|
||||
.filterIsInstance<CryptoCurrency.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) {
|
||||
// todo after move foundToken to CryptoCurrency model, use only id
|
||||
foundToken?.id == token.id.rawCurrencyId
|
||||
|
|
@ -621,7 +638,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
return currentCryptoCurrencies
|
||||
.filterIsInstance<CryptoCurrency.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
|
||||
}
|
||||
}
|
||||
|
|
@ -630,39 +647,43 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
when {
|
||||
isNetworkSelected() && type == AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> {
|
||||
val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isError = true,
|
||||
error = TextReference.Res(
|
||||
id = R.string.custom_token_creation_error_invalid_contract_address,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||
isError = true,
|
||||
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 -> {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(isError = false),
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = "", isEnabled = false),
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(isError = false),
|
||||
tokenNameInputField = state.form.tokenNameInputField.copy(value = "", isEnabled = false),
|
||||
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(
|
||||
value = "",
|
||||
isEnabled = false,
|
||||
),
|
||||
decimalsInputField = state.form.decimalsInputField.copy(value = "", isEnabled = false),
|
||||
),
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = "", isEnabled = false),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
|
|
@ -670,11 +691,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun getDerivationPath(): DerivationPath? {
|
||||
return when (uiState.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||
return when (uiState.value.form.derivationPathSelectorField?.selectedItem?.type) {
|
||||
DerivationPathSelectorType.CUSTOM ->
|
||||
createDerivationPathOrNull(uiState.form.derivationPathInputField?.value ?: "")
|
||||
createDerivationPathOrNull(uiState.value.form.derivationPathInputField?.value ?: "")
|
||||
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) {
|
||||
uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
} else {
|
||||
blockchain
|
||||
}
|
||||
|
|
@ -732,13 +753,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onContactAddressValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
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(
|
||||
address = enteredValue,
|
||||
blockchain = selectedNetwork,
|
||||
|
|
@ -746,14 +769,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
when (validatorResult) {
|
||||
is ContractAddressValidatorResult.Success -> {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
isError = false,
|
||||
isLoading = true,
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.contractAddressInputField.copy(
|
||||
isError = false,
|
||||
isLoading = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
updateForm(address = enteredValue, selectedNetwork = selectedNetwork)
|
||||
}
|
||||
|
||||
|
|
@ -766,76 +791,92 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun onNetworkSelectorItemClick(index: Int) {
|
||||
val selectedItem = requireNotNull(uiState.form.networkSelectorField.items.getOrNull(index))
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
networkSelectorField = uiState.form.networkSelectorField.copy(
|
||||
selectedItem = selectedItem,
|
||||
val state = uiState.updateAndGet { state ->
|
||||
val selectedItem = requireNotNull(state.form.networkSelectorField.items.getOrNull(index))
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
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(uiState.form.contractAddressInputField.value)
|
||||
)
|
||||
}
|
||||
|
||||
onContactAddressValueChange(state.form.contractAddressInputField.value)
|
||||
}
|
||||
|
||||
fun onTokenNameValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
tokenNameInputField = state.form.tokenNameInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onTokenSymbolValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
tokenSymbolInputField = state.form.tokenSymbolInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDecimalsValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
decimalsInputField = state.form.decimalsInputField.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onDerivationPathSelectorItemClick(index: Int) {
|
||||
val derivationSelector = requireNotNull(uiState.form.derivationPathSelectorField)
|
||||
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
||||
val derivationInputField = requireNotNull(uiState.form.derivationPathInputField)
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathSelectorField = derivationSelector.copy(
|
||||
selectedItem = selected,
|
||||
uiState.update { state ->
|
||||
val derivationSelector = requireNotNull(state.form.derivationPathSelectorField)
|
||||
val selected = requireNotNull(derivationSelector.items.getOrNull(index))
|
||||
val derivationInputField = requireNotNull(state.form.derivationPathInputField)
|
||||
|
||||
state.copySealed(
|
||||
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()
|
||||
}
|
||||
|
||||
fun onDerivationPathValueChange(enteredValue: String) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
derivationPathInputField = uiState.form.derivationPathInputField?.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
derivationPathInputField = state.form.derivationPathInputField?.copy(value = enteredValue),
|
||||
),
|
||||
)
|
||||
}
|
||||
updateFloatingButton()
|
||||
}
|
||||
|
||||
fun onAddCustomTokenClick() {
|
||||
if (!isNetworkSelected()) return
|
||||
val blockchain = uiState.form.networkSelectorField.selectedItem.blockchain
|
||||
val blockchain = uiState.value.form.networkSelectorField.selectedItem.blockchain
|
||||
when (getSupportBlockchainType(blockchain)) {
|
||||
SupportBlockchainType.SUPPORTED -> {
|
||||
/* no-op */
|
||||
|
|
@ -854,14 +895,16 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
CustomTokenType.TOKEN -> {
|
||||
val contractAddress = convertTokenAddress(
|
||||
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(
|
||||
token = Token(
|
||||
name = uiState.form.tokenNameInputField.value,
|
||||
symbol = uiState.form.tokenSymbolInputField.value,
|
||||
name = uiState.value.form.tokenNameInputField.value,
|
||||
symbol = uiState.value.form.tokenSymbolInputField.value,
|
||||
contractAddress = contractAddress,
|
||||
decimals = requireNotNull(uiState.form.decimalsInputField.value.toIntOrNull()),
|
||||
decimals = requireNotNull(uiState.value.form.decimalsInputField.value.toIntOrNull()),
|
||||
id = foundToken?.id,
|
||||
),
|
||||
network = blockchain,
|
||||
|
|
@ -878,71 +921,101 @@ internal class AddCustomTokenViewModel @Inject constructor(
|
|||
|
||||
analyticsSender.sendWhenAddTokenButtonClicked(currency)
|
||||
|
||||
viewModelScope.launch(dispatchers.io) {
|
||||
val oldButtonState = uiState.floatingButton
|
||||
uiState = uiState.copySealed(
|
||||
floatingButton = uiState.floatingButton.copy(isEnabled = false, showProgress = true),
|
||||
)
|
||||
runCatching { featureInteractor.saveToken(currency) }
|
||||
viewModelScope.launch {
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
floatingButton = state.floatingButton.copy(
|
||||
isEnabled = false,
|
||||
showProgress = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val result = featureInteractor.saveToken(currency)
|
||||
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
floatingButton = state.floatingButton.copy(
|
||||
isEnabled = true,
|
||||
showProgress = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
result
|
||||
.onSuccess { featureRouter.openWalletScreen() }
|
||||
.onFailure {
|
||||
uiState = uiState.copySealed(floatingButton = oldButtonState)
|
||||
Timber.e(it)
|
||||
}
|
||||
.onFailure { Timber.e(it, "Unable to save custom token") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertTokenAddress(blockchain: Blockchain, address: String): String {
|
||||
return when (blockchain) {
|
||||
Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address)
|
||||
else -> address
|
||||
/** Convert [address] to single address for specific [blockchain] or return null if invalid */
|
||||
private fun convertTokenAddress(blockchain: Blockchain, address: String): String? {
|
||||
return runCatching {
|
||||
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 {
|
||||
|
||||
fun onClearAddressButtonClick() {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = uiState.form.contractAddressInputField.copy(
|
||||
value = "",
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
error = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun onResetButtonClick() {
|
||||
with(uiState.form) {
|
||||
uiState = uiState.copySealed(
|
||||
form = uiState.form.copy(
|
||||
contractAddressInputField = contractAddressInputField.copy(
|
||||
uiState.update { state ->
|
||||
state.copySealed(
|
||||
form = state.form.copy(
|
||||
contractAddressInputField = state.form.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 = "",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import androidx.lifecycle.LifecycleCoroutineScope
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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 org.rekotlin.Action
|
||||
|
||||
|
|
@ -33,7 +31,8 @@ sealed class DetailsAction : Action {
|
|||
|
||||
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 ScanAndSaveUserWallet : DetailsAction() {
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
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.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
|
|
@ -148,7 +147,7 @@ class DetailsMiddleware {
|
|||
}
|
||||
.doOnFailure { error ->
|
||||
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
|
||||
Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
|
||||
}
|
||||
}
|
||||
.doOnResult {
|
||||
|
|
@ -474,10 +473,7 @@ class DetailsMiddleware {
|
|||
|
||||
if (isSameWallet) {
|
||||
store.dispatchOnMain(
|
||||
DetailsAction.PrepareCardSettingsData(
|
||||
scanResponse.card,
|
||||
scanResponse.cardTypesResolver,
|
||||
),
|
||||
DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
|
||||
)
|
||||
} else {
|
||||
store.dispatchDialogShow(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode
|
|||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
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.redux.AppState
|
||||
import com.tangem.tap.domain.extensions.signedHashesCount
|
||||
|
|
@ -29,11 +30,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
handlePrepareScreen(action, state)
|
||||
}
|
||||
is DetailsAction.PrepareCardSettingsData -> {
|
||||
handlePrepareCardSettingsScreen(
|
||||
card = action.card,
|
||||
cardTypesResolver = action.cardTypesResolver,
|
||||
state = detailsState,
|
||||
)
|
||||
handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
|
||||
}
|
||||
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
|
||||
is DetailsAction.ResetToFactory -> {
|
||||
|
|
@ -99,17 +96,17 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
|
|||
)
|
||||
}
|
||||
|
||||
private fun handlePrepareCardSettingsScreen(
|
||||
card: CardDTO,
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
state: DetailsState,
|
||||
): DetailsState {
|
||||
private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
|
||||
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||
val card = scanResponse.card
|
||||
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
|
||||
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
|
||||
|
||||
val cardSettingsState = CardSettingsState(
|
||||
cardInfo = card.toCardInfo(cardTypesResolver),
|
||||
scanResponse = scanResponse,
|
||||
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
|
||||
card = card,
|
||||
card = scanResponse.card,
|
||||
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
|
||||
resetButtonEnabled = false,
|
||||
condition1Checked = false,
|
||||
|
|
@ -126,6 +123,7 @@ private fun handlePrepareCardSettingsScreen(
|
|||
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
|
||||
dialog = null,
|
||||
)
|
||||
|
||||
return state.copy(cardSettingsState = cardSettingsState)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ data class AccessCodeRecoveryState(
|
|||
data class CardSettingsState(
|
||||
val cardInfo: CardInfo,
|
||||
val card: CardDTO,
|
||||
val scanResponse: ScanResponse,
|
||||
val manageSecurityState: ManageSecurityState?,
|
||||
val resetCardAllowed: Boolean,
|
||||
val resetButtonEnabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.tangem.common.routing.AppRoute
|
||||
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.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.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
|
||||
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.features.details.redux.CardSettingsState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
|
||||
|
|
@ -25,6 +32,7 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@HiltViewModel
|
||||
internal class ResetCardViewModel @Inject constructor(
|
||||
private val resetCardFeatureToggles: ResetCardFeatureToggles,
|
||||
|
|
@ -32,14 +40,17 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
private val resetCardUseCase: ResetCardUseCase,
|
||||
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
|
||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : ViewModel() {
|
||||
|
||||
private val currentUserWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
?: error("Selected user wallet can't be null")
|
||||
private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse
|
||||
?: error("ScanResponse can't be null")
|
||||
|
||||
private val currentUserWalletId = createUserWalletId(firstCardScanResponse)
|
||||
|
||||
// TODO: move logic to separate domain entity
|
||||
private val backupCardsCount = (currentUserWallet.getCardsCount() ?: 0) - 1
|
||||
private var resetCardsCount = 0
|
||||
private var resetBackupCardCount = 0
|
||||
|
||||
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
|
||||
val descriptionText = state?.cardInfo
|
||||
|
|
@ -84,7 +95,7 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
CardSettingsDialog.InterruptedResetDialog -> {
|
||||
ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
|
||||
onConfirmClick = ::onContinueResetClick,
|
||||
onDismiss = ::dismissAndFinishFullReset,
|
||||
onDismiss = ::onInterruptedResetDialogDismiss,
|
||||
)
|
||||
}
|
||||
CardSettingsDialog.CompletedResetDialog -> {
|
||||
|
|
@ -107,9 +118,9 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
|
||||
private fun makeFullReset() {
|
||||
viewModelScope.launch {
|
||||
resetCardUseCase(card = currentUserWallet.scanResponse.card).onRight {
|
||||
deleteSavedAccessCodesUseCase(currentUserWallet.cardId)
|
||||
deleteWalletUseCase(currentUserWallet.walletId)
|
||||
resetCardUseCase(card = firstCardScanResponse.card).onRight {
|
||||
deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId)
|
||||
deleteWalletUseCase(currentUserWalletId)
|
||||
|
||||
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
if (newSelectedWallet != null) {
|
||||
|
|
@ -128,12 +139,12 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
|
||||
viewModelScope.launch {
|
||||
resetCardUseCase(
|
||||
cardNumber = resetCardsCount + 1,
|
||||
card = currentUserWallet.scanResponse.card,
|
||||
userWalletId = currentUserWallet.walletId,
|
||||
cardNumber = resetBackupCardCount + 1,
|
||||
card = firstCardScanResponse.card,
|
||||
userWalletId = currentUserWalletId,
|
||||
)
|
||||
.onRight {
|
||||
resetCardsCount++
|
||||
resetBackupCardCount++
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
||||
|
|
@ -149,10 +160,23 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
showDialog(CardSettingsDialog.InterruptedResetDialog)
|
||||
}
|
||||
|
||||
private fun onInterruptedResetDialogDismiss() {
|
||||
analyticsEventHandler.send(Settings.CardSettings.FactoryResetCanceled(cardsCount = resetBackupCardCount + 1))
|
||||
|
||||
dismissAndFinishFullReset()
|
||||
}
|
||||
|
||||
private fun checkRemainingBackupCards() {
|
||||
val backupCardsCount = firstCardScanResponse.getBackupCardsCount()
|
||||
|
||||
when {
|
||||
backupCardsCount > resetCardsCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
|
||||
backupCardsCount == resetCardsCount -> showDialog(CardSettingsDialog.CompletedResetDialog)
|
||||
backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
|
||||
backupCardsCount == resetBackupCardCount -> {
|
||||
analyticsEventHandler.send(
|
||||
event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1),
|
||||
)
|
||||
showDialog(CardSettingsDialog.CompletedResetDialog)
|
||||
}
|
||||
else -> finishFullReset()
|
||||
}
|
||||
}
|
||||
|
|
@ -164,12 +188,17 @@ internal class ResetCardViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun finishFullReset() {
|
||||
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
val newSelectedWallet = userWalletsListManager.selectedUserWalletSync
|
||||
|
||||
if (newSelectedWallet != null) {
|
||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||
} 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() {
|
||||
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 {
|
||||
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> = { _, _ ->
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.TangemThemePreview
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ sealed class AnalyticsParam {
|
|||
data object Send : ScreensSources("Send")
|
||||
data object Intro : ScreensSources("Introduction")
|
||||
data object MyWallets : ScreensSources("My Wallets")
|
||||
data object Token : ScreensSources("Token")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,9 @@ data class ExchangeProvider(
|
|||
|
||||
@Json(name = "privacyPolicy")
|
||||
val privacyPolicy: String?,
|
||||
|
||||
@Json(name = "recommended")
|
||||
val isRecommended: Boolean = false,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
|
|
@ -31,4 +34,7 @@ enum class ExchangeProviderType {
|
|||
|
||||
@Json(name = "cex")
|
||||
CEX,
|
||||
|
||||
@Json(name = "dex-bridge")
|
||||
DEX_BRIDGE,
|
||||
}
|
||||
|
|
@ -23,34 +23,43 @@ data class ExchangeStatusResponse(
|
|||
enum class ExchangeStatus {
|
||||
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
New,
|
||||
|
||||
@Json(name = "waiting")
|
||||
WAITING,
|
||||
Waiting,
|
||||
|
||||
@Json(name = "confirming")
|
||||
CONFIRMING,
|
||||
Confirming,
|
||||
|
||||
@Json(name = "exchanging")
|
||||
EXCHANGING,
|
||||
Exchanging,
|
||||
|
||||
@Json(name = "sending")
|
||||
SENDING,
|
||||
Sending,
|
||||
|
||||
@Json(name = "finished")
|
||||
FINISHED,
|
||||
Finished,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
Failed,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
Refunded,
|
||||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
Verifying,
|
||||
|
||||
@Json(name = "expired")
|
||||
CANCELLED,
|
||||
Cancelled,
|
||||
|
||||
@Json(name = "waiting-tx-hash")
|
||||
WaitingTxHash,
|
||||
|
||||
@Json(name = "tx-failed")
|
||||
TxFailed,
|
||||
|
||||
@Json(name = "unknown")
|
||||
Unknown,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
|
|
|
|||
|
|
@ -53,14 +53,6 @@ object PreferencesKeys {
|
|||
|
||||
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 {
|
||||
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
|
||||
}
|
||||
|
|
@ -93,6 +85,14 @@ object PreferencesKeys {
|
|||
|
||||
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")
|
||||
|
||||
// region Permission
|
||||
|
|
|
|||
|
|
@ -235,6 +235,7 @@
|
|||
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
|
||||
<string name="express_exchange_status_title">Статус обмена</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_fetch_best_rates">Получение наилучших курсов...</string>
|
||||
<string name="express_floating_rate">Плавающая ставка</string>
|
||||
|
|
@ -248,6 +249,7 @@
|
|||
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="express_provider_recommended">Рекомендовано</string>
|
||||
<string name="express_terms_of_use">Условиями использования</string>
|
||||
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
|
||||
<string name="express_transaction_id">ID: %s</string>
|
||||
|
|
@ -584,6 +586,8 @@
|
|||
<string name="story_meet_title">Встречайте Tangem</string>
|
||||
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</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_dex_description">В сумму включена комиссия провайдера сервиса.</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_title">Exchange status</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_fetch_best_rates">Fetching best rates...</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_not_available">Unavailable for this pair</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_token_list_empty_search">No tokens found. Please try another request</string>
|
||||
<string name="express_transaction_id">ID: %s</string>
|
||||
|
|
@ -610,6 +612,8 @@
|
|||
<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_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_dex_description">The amount includes the service provider\'s fee.</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
|
||||
"zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_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
|
||||
"moonriver", "moonriver/test" -> R.drawable.img_moonriver_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
|
||||
"zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_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
|
||||
"moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22
|
||||
"mantle", "mantle/test" -> R.drawable.ic_mantle_22
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ class TangemColors internal constructor(
|
|||
warning: Color,
|
||||
attention: Color,
|
||||
accent: Color = TangemColorPalette.Azure,
|
||||
constant: Color = TangemColorPalette.White,
|
||||
) {
|
||||
var primary1 by mutableStateOf(primary1)
|
||||
private set
|
||||
|
|
@ -100,6 +101,7 @@ class TangemColors internal constructor(
|
|||
private set
|
||||
var attention by mutableStateOf(attention)
|
||||
private set
|
||||
var constant by mutableStateOf(constant)
|
||||
|
||||
fun update(other: Icon) {
|
||||
primary1 = other.primary1
|
||||
|
|
@ -110,6 +112,7 @@ class TangemColors internal constructor(
|
|||
accent = other.accent
|
||||
warning = other.warning
|
||||
attention = other.attention
|
||||
constant = other.constant
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.screen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
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
|
||||
* 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]
|
||||
*/
|
||||
internal interface CardSdkProvider {
|
||||
interface CardSdkProvider {
|
||||
|
||||
/** CardSDK instance */
|
||||
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()
|
||||
}
|
||||
|
||||
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 const val CHANGELLY_NAME = "changelly"
|
||||
private const val TRAVALA = "travala"
|
||||
// private const val OKX = "okx"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
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_WALLET_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_OKX_SHOW_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.store
|
||||
|
|
@ -16,23 +16,23 @@ class DefaultPromoSettingsRepository(
|
|||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : PromoSettingsRepository {
|
||||
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> {
|
||||
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() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowTokenSwapPromo() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
|
||||
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,8 +87,11 @@ internal object TokensDataModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDefaultMarketCoinsRepository(expressAssetsStore: ExpressAssetsStore): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore)
|
||||
fun provideDefaultMarketCoinsRepository(
|
||||
expressAssetsStore: ExpressAssetsStore,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): MarketCryptoCurrencyRepository {
|
||||
return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -314,26 +314,28 @@ internal class DefaultCurrenciesRepository(
|
|||
networkId: Network.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
): CryptoCurrency.Coin {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
|
||||
return withContext(dispatchers.io) {
|
||||
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)) {
|
||||
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
|
||||
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
|
||||
"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> {
|
||||
|
|
@ -395,29 +397,31 @@ internal class DefaultCurrenciesRepository(
|
|||
}
|
||||
|
||||
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
return when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
||||
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
||||
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
||||
is FeePaidSdkCurrency.Token -> {
|
||||
val balance = walletManagersFacade.tokenBalance(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
decimals = feePaidCurrency.token.decimals,
|
||||
id = feePaidCurrency.token.id,
|
||||
)
|
||||
FeePaidCurrency.Token(
|
||||
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
balance = balance,
|
||||
)
|
||||
return withContext(dispatchers.io) {
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
|
||||
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
|
||||
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
|
||||
is FeePaidSdkCurrency.Token -> {
|
||||
val balance = walletManagersFacade.tokenBalance(
|
||||
userWalletId = userWalletId,
|
||||
network = currency.network,
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
contractAddress = feePaidCurrency.token.contractAddress,
|
||||
decimals = feePaidCurrency.token.decimals,
|
||||
id = feePaidCurrency.token.id,
|
||||
)
|
||||
FeePaidCurrency.Token(
|
||||
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
|
||||
name = feePaidCurrency.token.name,
|
||||
symbol = feePaidCurrency.token.symbol,
|
||||
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.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DefaultMarketCryptoCurrencyRepository(
|
||||
private val expressAssetsStore: ExpressAssetsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MarketCryptoCurrencyRepository {
|
||||
|
||||
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
|
||||
|
|
@ -15,11 +18,16 @@ class DefaultMarketCryptoCurrencyRepository(
|
|||
}
|
||||
|
||||
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 {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}?.exchangeAvailable ?: false
|
||||
val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find {
|
||||
it.network == cryptoCurrency.network.backendId &&
|
||||
it.contractAddress.equals(contractAddress, ignoreCase = true)
|
||||
}
|
||||
|
||||
asset?.exchangeAvailable ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,6 +51,19 @@ internal class DefaultQuotesRepository(
|
|||
.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> {
|
||||
return withContext(dispatchers.io) {
|
||||
val selectedAppCurrency = requireNotNull(
|
||||
|
|
|
|||
|
|
@ -35,16 +35,19 @@ class DefaultTxHistoryRepository(
|
|||
private val sdkPageConverter by lazy { SdkPageConverter() }
|
||||
|
||||
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = currency,
|
||||
)
|
||||
return when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
|
||||
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
|
||||
is TxHistoryState.Success.HasTransactions -> state.txCount
|
||||
return withContext(dispatchers.io) {
|
||||
val userWallet = getUserWallet(userWalletId)
|
||||
val state = walletManagersFacade.getTxHistoryState(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = currency,
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
|
||||
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
|
||||
|
||||
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.lceLoading
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.launch
|
||||
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].
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
|
||||
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,
|
||||
) : Raise<E>, CoroutineScope by scope {
|
||||
) : Raise<E>, CoroutineScope by producerScope {
|
||||
|
||||
/**
|
||||
* Raises an [Lce] instance within the [ProducerScope].
|
||||
* It closes the [ProducerScope] after raise.
|
||||
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
|
||||
* 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 {
|
||||
scope.launch(NonCancellable) {
|
||||
scope.send(r.lceError())
|
||||
scope.close()
|
||||
}
|
||||
producerScope.trySendBlocking(r.lceError())
|
||||
producerScope.close()
|
||||
|
||||
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.
|
||||
* 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 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()
|
||||
}
|
||||
|
||||
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>) {
|
||||
scope.send(value)
|
||||
if (producerScope.isClosedForSend) return
|
||||
|
||||
producerScope.send(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +114,7 @@ fun <E : Any, C : Any> lceFlow(
|
|||
lce {
|
||||
val scope = LceFlowScope(
|
||||
raise = this@lce,
|
||||
scope = this@channelFlow,
|
||||
producerScope = this@channelFlow,
|
||||
ifLoading = ifLoading,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ interface CardTypesResolver {
|
|||
|
||||
fun isVoltInuWallet(): Boolean
|
||||
|
||||
fun isVividWallet(): Boolean
|
||||
|
||||
fun isPastelWallet(): Boolean
|
||||
|
||||
fun isWhiteWallet(): Boolean
|
||||
|
||||
fun isWallet2(): Boolean
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ internal class TangemCardTypesResolver(
|
|||
|
||||
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 {
|
||||
return walletData == null && card.firmwareVersion <= FirmwareVersion.HDWalletAvailable
|
||||
}
|
||||
|
|
@ -190,5 +198,13 @@ internal class TangemCardTypesResolver(
|
|||
const val COQ_WALLET_BATCH_ID = "AF28"
|
||||
const val COIN_METRICA_WALLET_BATCH_ID = "AF27"
|
||||
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
|
||||
}
|
||||
|
||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||
val wallet = walletManager.wallet
|
||||
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
|
||||
withContext(dispatchers.io) {
|
||||
walletManagersStore.remove(userWalletId) { walletManager ->
|
||||
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,
|
||||
networkTokens: List<CryptoCurrency.Token>,
|
||||
) {
|
||||
val walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(network.id.value),
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return
|
||||
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
||||
withContext(dispatchers.io) {
|
||||
val walletManager = walletManagersStore.getSyncOrNull(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = Blockchain.fromId(network.id.value),
|
||||
derivationPath = network.derivationPath.value,
|
||||
) ?: return@withContext
|
||||
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
|
||||
|
||||
tokensToRemove.forEach { token ->
|
||||
walletManager.removeToken(token)
|
||||
tokensToRemove.forEach { token ->
|
||||
walletManager.removeToken(token)
|
||||
}
|
||||
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
|
||||
walletManagersStore.store(userWalletId, walletManager)
|
||||
}
|
||||
|
||||
override suspend fun updatePendingTransactions(
|
||||
|
|
@ -576,12 +580,16 @@ class DefaultWalletManagersFacade(
|
|||
userWalletId: UserWalletId,
|
||||
currency: CryptoCurrency,
|
||||
): AssetRequirementsCondition? {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) {
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
val condition = walletManager.requirementsCondition(currencyType) ?: return null
|
||||
return requirementsConditionConverter.convert(condition)
|
||||
val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null
|
||||
requirementsConditionConverter.convert(condition)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun associateAsset(
|
||||
|
|
@ -589,15 +597,18 @@ class DefaultWalletManagersFacade(
|
|||
currency: CryptoCurrency,
|
||||
signer: CommonSigner,
|
||||
): SimpleResult {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
return withContext(dispatchers.io) {
|
||||
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
|
||||
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
|
||||
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return SimpleResult.Failure(
|
||||
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
|
||||
)
|
||||
if (walletManager !is AssetRequirementsManager) {
|
||||
return@withContext SimpleResult.Failure(
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,43 @@
|
|||
package com.tangem.domain.tokens.models.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class TokenSwapPromoAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Swap Promo", event, params, null) {
|
||||
|
||||
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
|
||||
|
||||
class Exchange(
|
||||
token: String,
|
||||
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
||||
class NoticePromotionBanner(
|
||||
source: AnalyticsParam.ScreensSources,
|
||||
programName: ProgramName,
|
||||
) : TokenSwapPromoAnalyticsEvent(
|
||||
event = "Button - Exchange Now",
|
||||
params = mapOf("Token" to token),
|
||||
event = "Notice - Promotion Banner",
|
||||
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.either
|
||||
import arrow.core.toNonEmptyListOrNull
|
||||
import com.tangem.domain.tokens.error.AddCurrencyError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -32,9 +31,9 @@ class AddCryptoCurrenciesUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @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))
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +46,13 @@ class AddCryptoCurrenciesUseCase(
|
|||
* @param userWalletId The ID of the user's wallet.
|
||||
* @param cryptoCurrency Token to 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(
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency.Token,
|
||||
network: Network,
|
||||
): Either<AddCurrencyError, Unit> = either {
|
||||
): Either<Throwable, Unit> = either {
|
||||
val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network)
|
||||
invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
||||
}
|
||||
|
|
@ -66,14 +65,14 @@ class AddCryptoCurrenciesUseCase(
|
|||
*
|
||||
* @param userWalletId The ID of the user's wallet.
|
||||
* @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(
|
||||
userWalletId: UserWalletId,
|
||||
currencies: List<CryptoCurrency>,
|
||||
): Either<AddCurrencyError, Unit> = either {
|
||||
): Either<Throwable, Unit> = either {
|
||||
val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) {
|
||||
raise(AddCurrencyError.DataError(it))
|
||||
raise(it)
|
||||
}
|
||||
val currenciesToAdd = currencies
|
||||
.filterNot(existingCurrencies::contains)
|
||||
|
|
@ -81,7 +80,7 @@ class AddCryptoCurrenciesUseCase(
|
|||
?: return@either
|
||||
|
||||
catch({ currenciesRepository.addCurrencies(userWalletId, currenciesToAdd) }) {
|
||||
raise(AddCurrencyError.DataError(it))
|
||||
raise(it)
|
||||
}
|
||||
|
||||
refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies)
|
||||
|
|
@ -91,7 +90,7 @@ class AddCryptoCurrenciesUseCase(
|
|||
* Refreshes the network statuses for tokens that have corresponding coins in the
|
||||
* [existingCurrencies] list.
|
||||
*/
|
||||
private suspend fun Raise<AddCurrencyError>.refreshUpdatedNetworks(
|
||||
private suspend fun Raise<Throwable>.refreshUpdatedNetworks(
|
||||
userWalletId: UserWalletId,
|
||||
currenciesToAdd: 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?> {
|
||||
val currency = currencyStatus.currency
|
||||
val cryptoStatuses = operations.getCurrenciesStatusesSync()
|
||||
val promoBanner = promoRepository.getChangellyPromoBanner()
|
||||
val promoBanner = promoRepository.getOkxPromoBanner()
|
||||
return combine(
|
||||
showSwapPromoTokenUseCase().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 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 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()
|
||||
.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 {
|
||||
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 androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
|
|
@ -60,7 +61,9 @@ internal class SendFragment : ComposeFragment() {
|
|||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
|
||||
SendScreen(viewModel.uiState, currentState.value)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SendScreen(uiState, currentState.value)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
|
|||
|
|
@ -2,9 +2,6 @@ package com.tangem.features.send.impl.presentation.viewmodel
|
|||
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
|
|
@ -126,7 +123,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val stateFactory = SendStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
|
|
@ -136,7 +133,7 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val recipientStateFactory = RecipientSendFactory(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
isUtxoConsolidationAvailableProvider = Provider { isUtxoConsolidationAvailable },
|
||||
validateWalletMemoUseCase = validateWalletMemoUseCase,
|
||||
|
|
@ -144,14 +141,14 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private val amountStateFactory = AmountStateFactory(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
)
|
||||
|
||||
private val feeStateFactory = FeeStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
|
||||
isFeeApproximateUseCase = isFeeApproximateUseCase,
|
||||
|
|
@ -160,13 +157,13 @@ internal class SendViewModel @Inject constructor(
|
|||
private val eventStateFactory = SendEventStateFactory(
|
||||
clickIntents = this,
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeStateFactory = feeStateFactory,
|
||||
)
|
||||
|
||||
private val feeNotificationFactory = FeeNotificationFactory(
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
clickIntents = this,
|
||||
)
|
||||
|
|
@ -174,7 +171,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendNotificationFactory = SendNotificationFactory(
|
||||
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
|
||||
feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
userWalletProvider = Provider { userWallet },
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
isSubtractAvailableProvider = Provider { isAmountSubtractAvailable },
|
||||
|
|
@ -189,15 +186,16 @@ internal class SendViewModel @Inject constructor(
|
|||
private val sendScreenAnalyticSender by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SendScreenAnalyticSender(
|
||||
stateRouterProvider = Provider { stateRouter },
|
||||
currentStateProvider = Provider { uiState },
|
||||
currentStateProvider = Provider { uiState.value },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
cryptoCurrencyProvider = Provider { cryptoCurrency },
|
||||
)
|
||||
}
|
||||
|
||||
// todo convert to StateFlow
|
||||
var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState())
|
||||
private set
|
||||
val uiState: MutableStateFlow<SendUiState> = MutableStateFlow(
|
||||
value = stateFactory.getInitialState(),
|
||||
)
|
||||
|
||||
private var userWallet: UserWallet by Delegates.notNull()
|
||||
private var userWallets: List<AvailableWallet> = emptyList()
|
||||
|
|
@ -274,7 +272,7 @@ internal class SendViewModel @Inject constructor(
|
|||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
uiState = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
uiState.value = stateFactory.getOnHideBalanceState(isBalanceHidden = it.isBalanceHidden)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(balanceHidingJobHolder)
|
||||
|
|
@ -347,15 +345,15 @@ internal class SendViewModel @Inject constructor(
|
|||
feeCryptoCurrencyStatus = feeCurrencyStatus
|
||||
subscribeOnQRScannerResult()
|
||||
when {
|
||||
uiState.sendState?.isSuccess == true -> return
|
||||
uiState.value.sendState?.isSuccess == true -> return
|
||||
transactionId != null && amount != null && destinationAddress != null -> {
|
||||
loadFee()
|
||||
uiState = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo)
|
||||
stateRouter.showSend()
|
||||
updateNotifications()
|
||||
}
|
||||
else -> {
|
||||
uiState = stateFactory.getReadyState()
|
||||
uiState.value = stateFactory.getReadyState()
|
||||
getWalletsAndRecent()
|
||||
stateRouter.showRecipient()
|
||||
updateNotifications()
|
||||
|
|
@ -379,9 +377,9 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
}.onSuccess { result ->
|
||||
userWallets = result
|
||||
uiState = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||
uiState.value = recipientStateFactory.onLoadedWalletsList(wallets = userWallets)
|
||||
}.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,
|
||||
).getOrElse { emptyList() }
|
||||
}
|
||||
uiState = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
||||
uiState.value = recipientStateFactory.onLoadedHistoryList(txHistory = txHistoryList)
|
||||
}
|
||||
|
||||
private fun onStateActive() {
|
||||
|
|
@ -430,7 +428,7 @@ internal class SendViewModel @Inject constructor(
|
|||
SendUiStateType.EditFee,
|
||||
-> loadFee()
|
||||
SendUiStateType.Send -> {
|
||||
uiState = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
||||
uiState.value = stateFactory.getIsAmountSubtractedState(isAmountSubtractAvailable)
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
}
|
||||
else -> Unit
|
||||
|
|
@ -443,7 +441,7 @@ internal class SendViewModel @Inject constructor(
|
|||
sendNotificationFactory.create()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = stateFactory.getSendNotificationState(notifications = it) }
|
||||
.onEach { uiState.value = stateFactory.getSendNotificationState(notifications = it) }
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(sendNotificationsJobHolder)
|
||||
|
|
@ -453,7 +451,7 @@ internal class SendViewModel @Inject constructor(
|
|||
feeNotificationFactory.create()
|
||||
.conflate()
|
||||
.distinctUntilChanged()
|
||||
.onEach { uiState = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||
.onEach { uiState.value = feeStateFactory.getFeeNotificationState(notifications = it) }
|
||||
.flowOn(dispatchers.main)
|
||||
.launchIn(viewModelScope)
|
||||
.saveIn(feeNotificationsJobHolder)
|
||||
|
|
@ -463,7 +461,7 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun popBackStack() = stateRouter.popBackStack()
|
||||
override fun onBackClick() {
|
||||
cancelFeeRequest()
|
||||
stateRouter.onBackClick(isSuccess = uiState.sendState?.isSuccess == true)
|
||||
stateRouter.onBackClick(isSuccess = uiState.value.sendState?.isSuccess == true)
|
||||
}
|
||||
|
||||
override fun onCloseClick() {
|
||||
|
|
@ -479,8 +477,8 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
override fun onNextClick(isFromEdit: Boolean) {
|
||||
val currentState = stateRouter.currentState.value
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
||||
sendScreenAnalyticSender.send(currentState.type, uiState)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit)
|
||||
sendScreenAnalyticSender.send(currentState.type, uiState.value)
|
||||
when (currentState.type) {
|
||||
SendUiStateType.Fee,
|
||||
SendUiStateType.EditFee,
|
||||
|
|
@ -507,9 +505,9 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onFailedTxEmailClick(errorMessage: String) {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value
|
||||
val feeValue = uiState.feeState?.fee?.amount?.value
|
||||
val amountValue = (uiState.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
val recipient = uiState.value.recipientState?.addressTextField?.value
|
||||
val feeValue = uiState.value.feeState?.fee?.amount?.value
|
||||
val amountValue = (uiState.value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
|
||||
|
||||
val receivingAmount = if (amountValue != null && feeValue != null) {
|
||||
checkAndCalculateSubtractedAmount(
|
||||
|
|
@ -517,7 +515,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
@ -539,20 +537,20 @@ internal class SendViewModel @Inject constructor(
|
|||
innerRouter.openTokenDetails(userWalletId, currency)
|
||||
|
||||
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
|
||||
if (checkIfFeeTooLow(feeSelectorState)) {
|
||||
uiState = eventStateFactory.getFeeTooLowAlert(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
uiState.value = eventStateFactory.getFeeTooLowAlert(
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
return true
|
||||
}
|
||||
return checkIfFeeTooHigh(
|
||||
feeSelectorState = feeSelectorState,
|
||||
onShow = { diff ->
|
||||
uiState = eventStateFactory.getFeeTooHighAlert(
|
||||
uiState.value = eventStateFactory.getFeeTooHighAlert(
|
||||
diff = diff,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -584,20 +582,20 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region amount state clicks
|
||||
override fun onCurrencyChangeClick(isFiat: Boolean) {
|
||||
uiState = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||
uiState.value = amountStateFactory.getOnCurrencyChangedState(isFiat)
|
||||
}
|
||||
|
||||
override fun onAmountValueChange(value: String) {
|
||||
uiState = amountStateFactory.getOnAmountValueChange(value)
|
||||
uiState.value = amountStateFactory.getOnAmountValueChange(value)
|
||||
}
|
||||
|
||||
override fun onMaxValueClick() {
|
||||
uiState = amountStateFactory.getOnMaxAmountClick()
|
||||
uiState.value = amountStateFactory.getOnMaxAmountClick()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.MaxAmountButtonClicked)
|
||||
}
|
||||
|
||||
override fun onAmountPasteTriggerDismiss() {
|
||||
uiState = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
||||
uiState.value = amountStateFactory.getOnAmountPastedTriggerDismiss()
|
||||
}
|
||||
// endregion
|
||||
|
||||
|
|
@ -606,10 +604,10 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource?) {
|
||||
viewModelScope.launch {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
uiState.value = recipientStateFactory.onRecipientAddressValueChange(value, isValuePasted = type != null)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val isValidAddress = validateAddress(value)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidState(value, isValidAddress)
|
||||
type?.let {
|
||||
analyticsEventHandler.send(
|
||||
SendAnalyticEvents.AddressEntered(
|
||||
|
|
@ -626,11 +624,11 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) {
|
||||
viewModelScope.launch {
|
||||
if (!checkIfXrpAddressValue(value)) {
|
||||
uiState = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val recipientState = uiState.getRecipientState(stateRouter.isEditState)
|
||||
uiState.value = recipientStateFactory.getOnRecipientMemoValueChange(value, isValuePasted)
|
||||
uiState.value = recipientStateFactory.getOnRecipientAddressValidationStarted()
|
||||
val recipientState = uiState.value.getRecipientState(stateRouter.isEditState)
|
||||
val maybeValidAddress = validateAddress(recipientState?.addressTextField?.value.orEmpty())
|
||||
uiState = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
||||
uiState.value = recipientStateFactory.getOnRecipientMemoValidState(value, maybeValidAddress.isRight())
|
||||
}
|
||||
}.saveIn(memoValidationJobHolder)
|
||||
}
|
||||
|
|
@ -652,21 +650,22 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private suspend fun checkIfXrpAddressValue(value: String): Boolean {
|
||||
return BlockchainUtils.decodeRippleXAddress(value, cryptoCurrency.network.id.value)?.let { decodedAddress ->
|
||||
uiState =
|
||||
uiState.value =
|
||||
recipientStateFactory.onRecipientAddressValueChange(value, isXAddress = true, isValuePasted = true)
|
||||
uiState = recipientStateFactory.getOnXAddressMemoState()
|
||||
uiState.value = recipientStateFactory.getOnXAddressMemoState()
|
||||
val isValidAddress = validateAddress(decodedAddress.address)
|
||||
uiState = recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||
uiState.value =
|
||||
recipientStateFactory.getOnRecipientAddressValidState(decodedAddress.address, isValidAddress)
|
||||
true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun onEnteredValidAddress(isNotValid: Boolean) {
|
||||
uiState = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
||||
uiState.value = recipientStateFactory.getHiddenRecentListState(isNotValid = isNotValid)
|
||||
}
|
||||
|
||||
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 isRecent = type == EnterAddressSource.RecentAddress
|
||||
|
|
@ -678,7 +677,7 @@ internal class SendViewModel @Inject constructor(
|
|||
override fun feeReload() = loadFee()
|
||||
|
||||
override fun onFeeSelectorClick(feeType: FeeType) {
|
||||
uiState = feeStateFactory.onFeeSelectedState(feeType)
|
||||
uiState.value = feeStateFactory.onFeeSelectedState(feeType)
|
||||
updateFeeNotifications()
|
||||
if (feeType == FeeType.Custom) {
|
||||
analyticsEventHandler.send(SendAnalyticEvents.CustomFeeButtonClicked)
|
||||
|
|
@ -686,7 +685,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCustomFeeValueChange(index: Int, value: String) {
|
||||
uiState = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
uiState.value = feeStateFactory.onCustomFeeValueChange(index, value)
|
||||
updateFeeNotifications()
|
||||
}
|
||||
|
||||
|
|
@ -702,14 +701,14 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private fun loadFee() {
|
||||
viewModelScope.launch {
|
||||
val isShowStatus = uiState.feeState?.fee == null
|
||||
val isShowStatus = uiState.value.feeState?.fee == null
|
||||
if (isShowStatus) {
|
||||
uiState = feeStateFactory.onFeeOnLoadingState()
|
||||
uiState.value = feeStateFactory.onFeeOnLoadingState()
|
||||
updateNotifications()
|
||||
}
|
||||
val result = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = feeStateFactory.onFeeOnLoadedState(it)
|
||||
uiState.value = feeStateFactory.onFeeOnLoadedState(it)
|
||||
sendIdleTimer = SystemClock.elapsedRealtime()
|
||||
},
|
||||
ifLeft = {
|
||||
|
|
@ -725,7 +724,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onFeeLoadFailed(isShowStatus: Boolean) {
|
||||
if (isShowStatus) uiState = feeStateFactory.onFeeOnErrorState()
|
||||
if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState()
|
||||
}
|
||||
|
||||
private suspend fun checkIfSubtractAvailable() {
|
||||
|
|
@ -744,8 +743,8 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
private suspend fun callFeeUseCase(): Either<GetFeeError, TransactionFee>? {
|
||||
val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation
|
||||
val amountState = uiState.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null
|
||||
val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null
|
||||
val amountState = uiState.value.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null
|
||||
val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null
|
||||
val amount = amountState.amountTextField.cryptoAmount.value ?: return null
|
||||
|
||||
return getFeeUseCase.invoke(
|
||||
|
|
@ -759,10 +758,10 @@ internal class SendViewModel @Inject constructor(
|
|||
|
||||
// region send state clicks
|
||||
override fun onSendClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
if (sendState.isSuccess) popBackStack()
|
||||
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) {
|
||||
verifyAndSendTransaction()
|
||||
} else {
|
||||
|
|
@ -772,22 +771,22 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun showAmount() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showAmount(isFromConfirmation = true)
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount))
|
||||
}
|
||||
|
||||
override fun showRecipient() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showRecipient(isFromConfirmation = true)
|
||||
uiState = stateFactory.getHiddenTapHelpState()
|
||||
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address))
|
||||
}
|
||||
|
||||
override fun showFee() {
|
||||
uiState = stateFactory.syncEditStates(isFromEdit = false)
|
||||
uiState.value = stateFactory.syncEditStates(isFromEdit = false)
|
||||
stateRouter.showFee(isFromConfirmation = true)
|
||||
setNeverToShowTapHelp()
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee))
|
||||
|
|
@ -798,7 +797,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onExploreClick() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked)
|
||||
innerRouter.openUrl(sendState.txUrl)
|
||||
}
|
||||
|
|
@ -813,7 +812,7 @@ internal class SendViewModel @Inject constructor(
|
|||
reduceAmountTo: BigDecimal?,
|
||||
clazz: Class<out SendNotification>,
|
||||
) {
|
||||
uiState = when {
|
||||
uiState.value = when {
|
||||
reduceAmountBy != null && reduceAmountByDiff != null -> amountStateFactory.getOnAmountReduceByState(
|
||||
reduceAmountBy = reduceAmountBy,
|
||||
reduceAmountByDiff = reduceAmountByDiff,
|
||||
|
|
@ -822,20 +821,22 @@ internal class SendViewModel @Inject constructor(
|
|||
else -> return
|
||||
}
|
||||
|
||||
uiState = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
uiState.value = sendNotificationFactory.dismissNotificationState(clazz)
|
||||
updateNotifications()
|
||||
}
|
||||
|
||||
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() {
|
||||
val recipient = uiState.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.feeState ?: return
|
||||
val recipient = uiState.value.recipientState?.addressTextField?.value ?: return
|
||||
val feeState = uiState.value.feeState ?: return
|
||||
val fee = feeState.fee ?: return
|
||||
val memo = uiState.recipientState?.memoTextField?.value
|
||||
val amountValue = (uiState.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: return
|
||||
val memo = uiState.value.recipientState?.memoTextField?.value
|
||||
val amountValue = (uiState.value.amountState as? AmountState.Data)
|
||||
?.amountTextField?.cryptoAmount?.value
|
||||
?: return
|
||||
val feeValue = fee.amount.value ?: return
|
||||
|
||||
val receivingAmount = checkAndCalculateSubtractedAmount(
|
||||
|
|
@ -843,7 +844,7 @@ internal class SendViewModel @Inject constructor(
|
|||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amountValue = amountValue,
|
||||
feeValue = feeValue,
|
||||
reduceAmountBy = uiState.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
reduceAmountBy = uiState.value.sendState?.reduceAmountBy ?: BigDecimal.ZERO,
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
|
|
@ -857,10 +858,10 @@ internal class SendViewModel @Inject constructor(
|
|||
).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it)
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = eventStateFactory.getGenericErrorState(
|
||||
error = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
ifRight = { txData ->
|
||||
|
|
@ -871,21 +872,23 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
|
||||
sendTransactionUseCase(
|
||||
val result = sendTransactionUseCase(
|
||||
txData = txData,
|
||||
userWallet = userWallet,
|
||||
network = cryptoCurrency.network,
|
||||
).fold(
|
||||
)
|
||||
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
|
||||
result.fold(
|
||||
ifLeft = { error ->
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState = eventStateFactory.getSendTransactionErrorState(
|
||||
uiState.value = eventStateFactory.getSendTransactionErrorState(
|
||||
error = error,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol))
|
||||
},
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
updateTransactionStatus(txData)
|
||||
addTokenToWalletIfNeeded()
|
||||
scheduleUpdates()
|
||||
|
|
@ -897,7 +900,7 @@ internal class SendViewModel @Inject constructor(
|
|||
private fun addTokenToWalletIfNeeded() {
|
||||
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 receivingUserWallet = userWallets.firstOrNull { it.address == destinationAddress } ?: return
|
||||
|
|
@ -916,7 +919,7 @@ internal class SendViewModel @Inject constructor(
|
|||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrElse { "" }
|
||||
uiState = stateFactory.getTransactionSendState(txData, txUrl)
|
||||
uiState.value = stateFactory.getTransactionSendState(txData, txUrl)
|
||||
}
|
||||
|
||||
private fun scheduleUpdates() {
|
||||
|
|
@ -934,7 +937,7 @@ internal class SendViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onCheckFeeUpdate() {
|
||||
val sendState = uiState.sendState ?: return
|
||||
val sendState = uiState.value.sendState ?: return
|
||||
val isSuccess = sendState.isSuccess
|
||||
val noErrorNotifications = sendState.notifications.none { it is SendNotification.Error }
|
||||
|
||||
|
|
@ -942,30 +945,30 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val feeUpdatedState = callFeeUseCase()?.fold(
|
||||
ifRight = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUpdatedAlert(
|
||||
fee = it,
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
onFeeNotIncreased = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = true)
|
||||
verifyAndSendTransaction()
|
||||
},
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUnreachableErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
uiState = if (feeUpdatedState != null) {
|
||||
uiState.value = if (feeUpdatedState != null) {
|
||||
feeUpdatedState
|
||||
} else {
|
||||
uiState = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
uiState.value = stateFactory.getSendingStateUpdate(isSending = false)
|
||||
eventStateFactory.getFeeUnreachableErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -976,12 +979,12 @@ internal class SendViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
neverShowTapHelpUseCase()
|
||||
}
|
||||
uiState = stateFactory.getHiddenTapHelpState()
|
||||
uiState.value = stateFactory.getHiddenTapHelpState()
|
||||
}
|
||||
|
||||
private fun showErrorAlert() {
|
||||
uiState = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState = eventStateFactory.onConsumeEventState() },
|
||||
uiState.value = eventStateFactory.getGenericErrorState(
|
||||
onConsume = { uiState.value = eventStateFactory.onConsumeEventState() },
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
imageLarge = exchangeProvider.imageLargeUrl,
|
||||
termsOfUse = exchangeProvider.termsOfUse,
|
||||
privacyPolicy = exchangeProvider.privacyPolicy,
|
||||
isRecommended = exchangeProvider.isRecommended,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -61,6 +62,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
imageLarge = additionalProvider.imageLargeUrl,
|
||||
termsOfUse = additionalProvider.termsOfUse,
|
||||
privacyPolicy = additionalProvider.privacyPolicy,
|
||||
isRecommended = additionalProvider.isRecommended,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +70,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
return when (type) {
|
||||
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
|
||||
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
|
||||
ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ data class ExchangeStatusModel(
|
|||
enum class ExchangeStatus {
|
||||
New,
|
||||
Waiting,
|
||||
WaitingTxHash,
|
||||
Confirming,
|
||||
Verifying,
|
||||
Exchanging,
|
||||
|
|
@ -19,4 +20,5 @@ enum class ExchangeStatus {
|
|||
Finished,
|
||||
Refunded,
|
||||
Cancelled,
|
||||
Unknown,
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ data class SavedSwapTransactionListModelInner(
|
|||
val transactions: List<SavedSwapTransactionModel>,
|
||||
)
|
||||
|
||||
// TODO refactor to use separate models to store
|
||||
data class SavedSwapTransactionModel(
|
||||
val txId: String,
|
||||
val timestamp: Long,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ data class CryptoCurrencySwapInfo(
|
|||
*
|
||||
* @property providerId provider id
|
||||
* @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(
|
||||
val providerId: String,
|
||||
|
|
@ -34,11 +38,13 @@ data class SwapProvider(
|
|||
val imageLarge: String,
|
||||
val termsOfUse: String?,
|
||||
val privacyPolicy: String?,
|
||||
val isRecommended: Boolean = false,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
DEX,
|
||||
CEX,
|
||||
enum class ExchangeProviderType(val providerName: String) {
|
||||
DEX("DEX"),
|
||||
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.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.demo.DemoConfig
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
|
||||
import com.tangem.domain.tokens.model.*
|
||||
import com.tangem.domain.tokens.model.FeePaidCurrency
|
||||
|
|
@ -60,6 +61,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val createTransactionUseCase: CreateTransactionUseCase,
|
||||
private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val currencyChecksRepository: CurrencyChecksRepository,
|
||||
|
|
@ -275,7 +277,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
|
||||
val networkId = fromToken.currency.network.backendId
|
||||
when (provider.type) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
manageDex(
|
||||
networkId = networkId,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -349,7 +351,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
} else {
|
||||
provider to getQuotesState(
|
||||
exchangeProviderType = ExchangeProviderType.DEX,
|
||||
exchangeProviderType = provider.type,
|
||||
quoteDataModel = quotes,
|
||||
amount = amount,
|
||||
fromToken = fromToken,
|
||||
|
|
@ -420,13 +422,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
fromToken.network.backendId,
|
||||
fromToken.network.derivationPath.value,
|
||||
) ?: ProxyAmount.empty()
|
||||
// ignore if amount is bigger than balance
|
||||
if (amount.value > nativeBalance.value) {
|
||||
return
|
||||
}
|
||||
val fee = when (txFee) {
|
||||
TxFeeState.Empty -> BigDecimal.ZERO
|
||||
is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue
|
||||
is TxFeeState.SingleFeeState -> txFee.fee.feeValue
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
@ -568,6 +574,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
fee: TxFee,
|
||||
): SwapTransactionState {
|
||||
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
|
||||
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
|
||||
|
||||
return when (swapProvider.type) {
|
||||
ExchangeProviderType.CEX -> {
|
||||
val amountDecimal = toBigDecimalOrNull(amountToSwap)
|
||||
|
|
@ -586,8 +595,9 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
userWalletId = requireNotNull(getSelectedWallet()).walletId,
|
||||
)
|
||||
}
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
onSwapDex(
|
||||
provider = swapProvider,
|
||||
networkId = currencyToSend.currency.network.backendId,
|
||||
swapData = requireNotNull(swapData),
|
||||
currencyToSendStatus = currencyToSend,
|
||||
|
|
@ -639,6 +649,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun onSwapDex(
|
||||
provider: SwapProvider,
|
||||
networkId: String,
|
||||
swapData: SwapDataModel,
|
||||
currencyToSendStatus: CryptoCurrencyStatus,
|
||||
|
|
@ -650,7 +661,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
|
||||
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
|
||||
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(
|
||||
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
|
||||
fee = getFeeForTransaction(
|
||||
|
|
@ -684,6 +696,17 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
txHash = txHash,
|
||||
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)
|
||||
SwapTransactionState.TxSent(
|
||||
fromAmount = amountFormatter.formatSwapAmountToUI(
|
||||
|
|
@ -700,16 +723,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
timestamp = System.currentTimeMillis(),
|
||||
)
|
||||
},
|
||||
ifLeft = {
|
||||
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
|
||||
}
|
||||
},
|
||||
ifLeft = { handleSendTxError(it) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -781,14 +795,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
val derivationPath = currencyToSend.currency.network.derivationPath.value
|
||||
return result.fold(
|
||||
ifLeft = {
|
||||
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
|
||||
}
|
||||
handleSendTxError(it)
|
||||
},
|
||||
ifRight = { txHash ->
|
||||
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 {
|
||||
val feeAmountValue = fee.feeValue
|
||||
val feeAmount = Amount(
|
||||
|
|
@ -882,11 +900,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
swapProvider: SwapProvider,
|
||||
swapDataModel: SwapDataModel,
|
||||
timestamp: Long,
|
||||
txExternalUrl: String,
|
||||
txExternalId: String,
|
||||
txExternalUrl: String? = null,
|
||||
txExternalId: String? = null,
|
||||
) {
|
||||
val selectedWallet = getSelectedWallet() ?: return
|
||||
swapTransactionRepository.storeTransaction(
|
||||
userWalletId = UserWalletId(userWalletManager.getWalletId()),
|
||||
userWalletId = selectedWallet.walletId,
|
||||
fromCryptoCurrency = currencyToSend.currency,
|
||||
toCryptoCurrency = currencyToGet.currency,
|
||||
transaction = SavedSwapTransactionModel(
|
||||
|
|
@ -1072,7 +1091,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
|
||||
when (exchangeProviderType) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
val state = updatePermissionState(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
|
|
@ -1407,19 +1426,25 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
swapAmount = swapAmount,
|
||||
spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" },
|
||||
)
|
||||
val feeData = try {
|
||||
transactionManager.getFee(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
||||
destinationAddress = fromToken.getContractAddress(),
|
||||
increaseBy = INCREASE_GAS_LIMIT_BY,
|
||||
data = transactionData,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get fee")
|
||||
null
|
||||
val userWallet = getSelectedWallet()
|
||||
val cardId = userWallet?.scanResponse?.card?.cardId
|
||||
val feeData = if (cardId != null && isDemoCardUseCase(cardId)) {
|
||||
getDemoFees(fromTokenStatus.currency)
|
||||
} else {
|
||||
try {
|
||||
transactionManager.getFee(
|
||||
networkId = networkId,
|
||||
amountToSend = BigDecimal.ZERO,
|
||||
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
|
||||
destinationAddress = fromToken.getContractAddress(),
|
||||
increaseBy = INCREASE_GAS_LIMIT_BY,
|
||||
data = transactionData,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to get fee")
|
||||
null
|
||||
}
|
||||
}
|
||||
val feeState = feeData?.let {
|
||||
when (feeData) {
|
||||
|
|
@ -1803,10 +1828,37 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
.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 {
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
|
||||
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
|
||||
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 createTransactionUseCase: CreateTransactionUseCase,
|
||||
createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase,
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
quotesRepository: QuotesRepository,
|
||||
swapTransactionRepository: SwapTransactionRepository,
|
||||
appCurrencyRepository: AppCurrencyRepository,
|
||||
|
|
@ -62,6 +63,7 @@ class SwapDomainModule {
|
|||
sendTransactionUseCase = sendTransactionUseCase,
|
||||
createTransactionUseCase = createTransactionUseCase,
|
||||
createTransactionExtrasUseCase = createTransactionDataExtrasUseCase,
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
quotesRepository = quotesRepository,
|
||||
swapTransactionRepository = swapTransactionRepository,
|
||||
appCurrencyRepository = appCurrencyRepository,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
|||
import com.tangem.feature.swap.domain.models.ui.FeeType
|
||||
|
||||
private const val SWAP_CATEGORY = "Swap"
|
||||
private const val PROMO_CATEGORY = "Promo"
|
||||
|
||||
sealed class SwapEvents(
|
||||
event: String,
|
||||
|
|
@ -17,7 +18,7 @@ sealed class SwapEvents(
|
|||
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(
|
||||
event = "Choose Token Screen Opened",
|
||||
|
|
@ -37,7 +38,7 @@ sealed class SwapEvents(
|
|||
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(
|
||||
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(
|
||||
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(
|
||||
event = "Provider Chosen",
|
||||
|
|
@ -88,7 +89,7 @@ sealed class SwapEvents(
|
|||
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(
|
||||
event = "Notice - Not Enough Fee",
|
||||
|
|
@ -110,4 +111,20 @@ sealed class SwapEvents(
|
|||
"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
|
||||
sealed class AdditionalBadge {
|
||||
object BestTrade : AdditionalBadge()
|
||||
object Empty : AdditionalBadge()
|
||||
object PermissionRequired : AdditionalBadge()
|
||||
data object BestTrade : AdditionalBadge()
|
||||
data object Empty : AdditionalBadge()
|
||||
data object PermissionRequired : AdditionalBadge()
|
||||
data object Recommended : AdditionalBadge()
|
||||
}
|
||||
|
||||
@Immutable
|
||||
|
|
@ -54,6 +55,7 @@ sealed class ProviderState {
|
|||
NONE, CLICK, SELECT
|
||||
}
|
||||
|
||||
// Prefix will be disabled in 5.12 but mechanism is still implemented
|
||||
@Immutable
|
||||
enum class PrefixType {
|
||||
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")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
|
|
@ -142,16 +143,12 @@ private fun ProviderContentState(
|
|||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
}
|
||||
val badgeModifier = Modifier.padding(start = TangemTheme.dimens.spacing4)
|
||||
when (state.additionalBadge) {
|
||||
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(
|
||||
Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(
|
||||
Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
ProviderState.AdditionalBadge.Empty -> {
|
||||
// no-op
|
||||
}
|
||||
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier)
|
||||
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier)
|
||||
ProviderState.AdditionalBadge.Recommended -> RecommendedItem(badgeModifier)
|
||||
ProviderState.AdditionalBadge.Empty -> Unit
|
||||
}
|
||||
}
|
||||
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
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
|
|
|
|||
|
|
@ -677,6 +677,7 @@ internal class StateBuilder(
|
|||
return when (dataError) {
|
||||
is DataError.ExchangeTooSmallAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_min_amount,
|
||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
|
|
@ -687,6 +688,7 @@ internal class StateBuilder(
|
|||
}
|
||||
is DataError.ExchangeTooBigAmountError -> {
|
||||
swapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider = swapProvider,
|
||||
alertText = resourceReference(
|
||||
R.string.express_provider_max_amount,
|
||||
wrappedList(dataError.amount.getFormattedCryptoAmount(fromToken)),
|
||||
|
|
@ -1005,13 +1007,15 @@ internal class StateBuilder(
|
|||
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
|
||||
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
|
||||
|
||||
val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName ||
|
||||
providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName
|
||||
return uiState.copy(
|
||||
successState = SwapSuccessStateHolder(
|
||||
timestamp = swapTransactionState.timestamp,
|
||||
txUrl = txUrl,
|
||||
providerName = stringReference(providerState.name),
|
||||
providerType = stringReference(providerState.type),
|
||||
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
|
||||
showStatusButton = shouldShowStatus,
|
||||
providerIcon = providerState.iconUrl,
|
||||
rate = providerState.subtitle,
|
||||
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
|
||||
|
|
@ -1093,7 +1097,7 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
val message = when (providerType) {
|
||||
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
|
||||
ExchangeProviderType.DEX -> {
|
||||
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
|
||||
val refs = buildList {
|
||||
if (isPriceImpact) {
|
||||
add(resourceReference(R.string.swapping_high_price_impact_description))
|
||||
|
|
@ -1540,7 +1544,9 @@ internal class StateBuilder(
|
|||
val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol
|
||||
val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol
|
||||
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
|
||||
} else {
|
||||
ProviderState.AdditionalBadge.Empty
|
||||
|
|
@ -1549,12 +1555,12 @@ internal class StateBuilder(
|
|||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = badge,
|
||||
selectionType = selectionType,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -1569,6 +1575,8 @@ internal class StateBuilder(
|
|||
val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency)
|
||||
val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) {
|
||||
ProviderState.AdditionalBadge.PermissionRequired
|
||||
} else if (isRecommended) {
|
||||
ProviderState.AdditionalBadge.Recommended
|
||||
} else {
|
||||
ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
|
|
@ -1576,7 +1584,7 @@ internal class StateBuilder(
|
|||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
subtitle = stringReference(rateString),
|
||||
additionalBadge = additionalBadge,
|
||||
selectionType = selectionType,
|
||||
|
|
@ -1605,24 +1613,26 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
private fun SwapProvider.convertToAvailableFromProviderState(
|
||||
swapProvider: SwapProvider,
|
||||
alertText: TextReference,
|
||||
selectionType: ProviderState.SelectionType,
|
||||
onProviderClick: (String) -> Unit,
|
||||
): ProviderState {
|
||||
val additionalBadge = if (swapProvider.isRecommended) {
|
||||
ProviderState.AdditionalBadge.Recommended
|
||||
} else {
|
||||
ProviderState.AdditionalBadge.Empty
|
||||
}
|
||||
return ProviderState.Content(
|
||||
id = this.providerId,
|
||||
name = this.name,
|
||||
iconUrl = this.imageLarge,
|
||||
type = this.type.toString(),
|
||||
type = this.type.providerName,
|
||||
selectionType = selectionType,
|
||||
subtitle = alertText,
|
||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||
additionalBadge = additionalBadge,
|
||||
percentLowerThenBest = PercentDifference.Empty,
|
||||
namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) {
|
||||
ProviderState.PrefixType.PROVIDED_BY
|
||||
} else {
|
||||
ProviderState.PrefixType.NONE
|
||||
},
|
||||
namePrefix = ProviderState.PrefixType.NONE,
|
||||
onProviderClick = onProviderClick,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ private val state = SwapSuccessStateHolder(
|
|||
txUrl = "https://www.google.com/#q=nam",
|
||||
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
|
||||
providerName = TextReference.Str("1inch"),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
|
||||
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
|
||||
showStatusButton = false,
|
||||
providerIcon = "",
|
||||
fromTokenAmount = TextReference.Str("1 000 DAI"),
|
||||
|
|
|
|||
|
|
@ -282,7 +282,10 @@ private fun Content(
|
|||
|
||||
if (amountEquivalent != null) {
|
||||
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) {
|
||||
Text(
|
||||
text = makePriceImpactBalanceWarning(
|
||||
|
|
@ -291,8 +294,6 @@ private fun Content(
|
|||
),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
)
|
||||
} else {
|
||||
AnimatedContent(targetState = amountEquivalent, label = "") {
|
||||
|
|
@ -312,7 +313,7 @@ private fun Content(
|
|||
modifier = Modifier.size(size = TangemTheme.dimens.size20),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_alert_24),
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = if (priceImpact is PriceImpact.Value) {
|
||||
TangemTheme.colors.text.attention
|
||||
|
|
|
|||
|
|
@ -435,8 +435,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) {
|
||||
currentSelected
|
||||
} else {
|
||||
findBestQuoteProvider(consideredProviders.getLastLoadedSuccessStates())
|
||||
?: consideredProviders.keys.first()
|
||||
val successLoadedData = consideredProviders.getLastLoadedSuccessStates()
|
||||
val recommendedProvider = successLoadedData.keys.firstOrNull { it.isRecommended }
|
||||
val bestQuotesProvider = findBestQuoteProvider(successLoadedData)
|
||||
triggerPromoProviderEvent(recommendedProvider, bestQuotesProvider)
|
||||
recommendedProvider ?: bestQuotesProvider ?: consideredProviders.keys.first()
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
const val loggingTag = "SwapViewModel"
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
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.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
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.components.TokenDetailsNotification
|
||||
|
||||
|
|
@ -35,12 +37,15 @@ internal class TokenDetailsNotificationsAnalyticsSender(
|
|||
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
|
||||
currency = cryptoCurrency,
|
||||
)
|
||||
is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
source = AnalyticsParam.ScreensSources.Token,
|
||||
)
|
||||
is TokenDetailsNotification.NetworksUnreachable,
|
||||
is TokenDetailsNotification.ExistentialDeposit,
|
||||
is TokenDetailsNotification.NetworksNoAccount,
|
||||
is TokenDetailsNotification.TopUpWithoutReserve,
|
||||
is TokenDetailsNotification.RentInfo,
|
||||
is TokenDetailsNotification.SwapPromo,
|
||||
is TokenDetailsNotification.NetworkShutdown,
|
||||
is TokenDetailsNotification.HederaAssociateWarning,
|
||||
is TokenDetailsNotification.KoinosMana,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
|
@ -45,16 +48,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
val onCloseClick: () -> Unit,
|
||||
) : TokenDetailsNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.token_swap_changelly_promotion_title,
|
||||
formatArgs = wrappedList("0%"),
|
||||
),
|
||||
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,
|
||||
title = resourceReference(id = R.string.swap_promo_title),
|
||||
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = onCloseClick,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
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> {
|
||||
if (status == null) return persistentListOf()
|
||||
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
||||
val isWaitingTxHash = status == ExchangeStatus.WaitingTxHash
|
||||
val isConfirming = status == ExchangeStatus.Confirming
|
||||
val isVerifying = status == ExchangeStatus.Verifying
|
||||
val isExchanging = status == ExchangeStatus.Exchanging
|
||||
|
|
@ -174,27 +175,35 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
val isExchangingDone = !isExchanging && isConfirmingDone
|
||||
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
|
||||
|
||||
return if (status == ExchangeStatus.Cancelled) {
|
||||
listOf(cancelledStep())
|
||||
} else {
|
||||
listOf(
|
||||
waitStep(isWaiting, isWaitingDone),
|
||||
confirmStep(isConfirming, isConfirmingDone),
|
||||
exchangeStep(
|
||||
isExchanging = isExchanging,
|
||||
isExchangingDone = isExchangingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
isVerifying = isVerifying,
|
||||
isFailed = isFailed,
|
||||
),
|
||||
sendStep(
|
||||
isSending = isSending,
|
||||
isSendingDone = isSendingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
),
|
||||
)
|
||||
return buildList {
|
||||
when {
|
||||
status == ExchangeStatus.Cancelled -> add(cancelledStep())
|
||||
isWaitingTxHash -> add(waitTxStep())
|
||||
// ExchangeStatus.Unknown is temporary added for 1inch
|
||||
status == ExchangeStatus.Unknown -> add(unknownStateStep())
|
||||
else -> {
|
||||
add(waitStep(isWaiting, isWaitingDone))
|
||||
add(confirmStep(isConfirming, isConfirmingDone))
|
||||
add(
|
||||
exchangeStep(
|
||||
isExchanging = isExchanging,
|
||||
isExchangingDone = isExchangingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
isVerifying = isVerifying,
|
||||
isFailed = isFailed,
|
||||
),
|
||||
)
|
||||
add(
|
||||
sendStep(
|
||||
isSending = isSending,
|
||||
isSendingDone = isSendingDone,
|
||||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
|
|
@ -216,6 +225,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
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(
|
||||
status = ExchangeStatus.Confirming,
|
||||
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.MarketPriceBlockState
|
||||
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.txHistoryItems
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
|
|
@ -116,7 +116,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
|||
contentType = { it.config::class.java },
|
||||
itemContent = {
|
||||
if (it is TokenDetailsNotification.SwapPromo) {
|
||||
NotificationWithBackground(
|
||||
OkxPromoNotification(
|
||||
config = it.config,
|
||||
modifier = itemModifier.animateItemPlacement(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
|||
SpacerH12()
|
||||
ExchangeProvider(
|
||||
providerName = TextReference.Str(config.provider.name),
|
||||
providerType = TextReference.Str(config.provider.type.name),
|
||||
providerType = TextReference.Str(config.provider.type.providerName),
|
||||
providerTxId = config.txExternalId,
|
||||
imageUrl = config.provider.imageLarge,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ internal class ExchangeStatusFactory(
|
|||
return when (status) {
|
||||
ExchangeStatus.New,
|
||||
ExchangeStatus.Waiting,
|
||||
ExchangeStatus.WaitingTxHash,
|
||||
ExchangeStatus.Sending,
|
||||
ExchangeStatus.Confirming,
|
||||
ExchangeStatus.Exchanging,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.blockchain.common.address.AddressType
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.bundle.unbundle
|
||||
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.global.BuyCurrencyDeepLink
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
|
|
@ -565,7 +566,13 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
ifRight = { it },
|
||||
)
|
||||
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() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
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() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent {
|
|||
|
||||
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.models.AnalyticsEvent
|
||||
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.state.model.WalletNotification
|
||||
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.RateApp -> MainScreen.HowDoYouLikeTangem
|
||||
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
|
||||
is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner(
|
||||
is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
|
||||
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.Informational.NoAccount,
|
||||
is WalletNotification.Warning.LowSignatures,
|
||||
is WalletNotification.Warning.SomeNetworksUnreachable,
|
||||
is WalletNotification.Warning.NetworksUnreachable,
|
||||
is WalletNotification.SwapPromo,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
|
|||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.promo.PromoBanner
|
||||
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.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val getTokenListUseCase: GetTokenListUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
|
||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
private val promoRepository: PromoRepository,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val backupValidator: BackupValidator,
|
||||
|
|
@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
|
||||
val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) }
|
||||
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
|
||||
return combine(
|
||||
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
|
||||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
|
||||
flow4 = shouldShowTravalaPromoWalletUseCase().conflate(),
|
||||
flow5 = travalaPromoFlow.conflate(),
|
||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner ->
|
||||
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
|
||||
flow5 = promoFlow.conflate(),
|
||||
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
|
||||
|
||||
readyForRateAppNotification = true
|
||||
buildList {
|
||||
addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents)
|
||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||
|
||||
addCriticalNotifications(userWallet)
|
||||
|
||||
|
|
@ -69,18 +69,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addTravalaPromoNotification(
|
||||
private fun MutableList<WalletNotification>.addSwapPromoNotification(
|
||||
shouldShowPromo: Boolean,
|
||||
promoBanner: PromoBanner?,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
promoBanner ?: return
|
||||
val promoNotification = WalletNotification.TravalaPromo(
|
||||
val promoNotification = WalletNotification.SwapPromo(
|
||||
startDateTime = promoBanner.bannerState.timeline.start,
|
||||
endDateTime = promoBanner.bannerState.timeline.end,
|
||||
bannerLink = promoBanner.bannerState.link,
|
||||
onBookNowButtonClick = clickIntents::onTravalaPromoClick,
|
||||
onCloseClick = clickIntents::onCloseTravalaPromoClick,
|
||||
onCloseClick = clickIntents::onCloseSwapPromoClick,
|
||||
)
|
||||
addIf(
|
||||
element = promoNotification,
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ fun UserWallet.getCardsCount(): Int? {
|
|||
return if (isMultiCurrency) {
|
||||
when (val status = scanResponse.card.backupStatus) {
|
||||
is CardDTO.BackupStatus.Active -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1
|
||||
is CardDTO.BackupStatus.NoBackup -> 1
|
||||
is CardDTO.BackupStatus.NoBackup,
|
||||
is CardDTO.BackupStatus.CardLinked,
|
||||
-> 1
|
||||
null -> 1 // Multi-currency wallet without backup function. Example, 4.12
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ internal object WalletImageResolver {
|
|||
cardTypesResolver.isCOQWallet() -> userWallet.resolveCOQWallet()
|
||||
cardTypesResolver.isCoinMetricaWallet() -> userWallet.resolveCoinMetricaWallet()
|
||||
cardTypesResolver.isVoltInuWallet() -> userWallet.resolveVoltInuWallet()
|
||||
cardTypesResolver.isVividWallet() -> userWallet.resolveVividWallet()
|
||||
cardTypesResolver.isPastelWallet() -> userWallet.resolvePastelWallet()
|
||||
cardTypesResolver.isWallet2() -> userWallet.resolveWallet2()
|
||||
cardTypesResolver.isShibaWallet() -> userWallet.resolveShibaWallet()
|
||||
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? {
|
||||
return resolveWalletWithBackups { count ->
|
||||
when (count) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ internal class WalletStateController @Inject constructor() {
|
|||
}
|
||||
|
||||
fun update(transformer: WalletScreenStateTransformer) {
|
||||
Timber.d("Applying: ${transformer::class.simpleName}")
|
||||
mutableUiState.update(function = transformer::transform)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.feature.wallet.impl.R
|
||||
import org.joda.time.DateTime
|
||||
|
|
@ -205,16 +208,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
val onCloseClick: () -> Unit,
|
||||
) : WalletNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.main_swap_changelly_promotion_title,
|
||||
formatArgs = wrappedList("0%"),
|
||||
),
|
||||
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,
|
||||
title = resourceReference(id = R.string.swap_promo_title),
|
||||
subtitle = resourceReference(id = R.string.swap_promo_text),
|
||||
iconResId = R.drawable.img_okx_dex_logo,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,22 +1,32 @@
|
|||
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.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.WalletTokensListState
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class SetTokenListErrorTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val selectedWallet: UserWallet,
|
||||
private val error: TokenListError,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
private val appCurrency: AppCurrency,
|
||||
) : WalletStateTransformer(selectedWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (error) {
|
||||
is TokenListError.EmptyTokens -> {
|
||||
when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(tokensListState = WalletTokensListState.Empty)
|
||||
prevState.copy(
|
||||
walletCardState = prevState.walletCardState.toLoadedState(),
|
||||
tokensListState = WalletTokensListState.Empty,
|
||||
)
|
||||
}
|
||||
is WalletState.MultiCurrency.Locked -> {
|
||||
Timber.w("Impossible to load tokens list for locked wallet")
|
||||
|
|
@ -37,4 +47,21 @@ internal class SetTokenListErrorTransformer(
|
|||
-> 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(),
|
||||
transform = { maybeTokenList, maybeAppCurrency ->
|
||||
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
||||
Timber.e("Failed to load app currency: $e")
|
||||
AppCurrency.Default
|
||||
}
|
||||
|
||||
val tokenList = maybeTokenList.getOrElse(
|
||||
ifLoading = { maybeContent ->
|
||||
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
|
||||
|
|
@ -74,16 +79,17 @@ internal abstract class BasicTokenListSubscriber(
|
|||
},
|
||||
ifError = { e ->
|
||||
Timber.e("Failed to load token list: $e")
|
||||
SetTokenListErrorTransformer(userWallet.walletId, e)
|
||||
stateHolder.update(
|
||||
SetTokenListErrorTransformer(
|
||||
selectedWallet = userWallet,
|
||||
error = e,
|
||||
appCurrency = appCurrency,
|
||||
),
|
||||
)
|
||||
return@combine
|
||||
},
|
||||
)
|
||||
|
||||
val appCurrency = maybeAppCurrency.getOrElse { e ->
|
||||
Timber.e("Failed to load app currency: $e")
|
||||
AppCurrency.Default
|
||||
}
|
||||
|
||||
updateContent(tokenList, appCurrency)
|
||||
walletWithFundsChecker.check(tokenList)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
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.res.TangemTheme
|
||||
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
|
||||
when (it) {
|
||||
is WalletNotification.SwapPromo -> {
|
||||
NotificationWithBackground(
|
||||
OkxPromoNotification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import arrow.core.getOrElse
|
|||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
|
||||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
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.WalletEvent
|
||||
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.transformers.*
|
||||
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 walletDeepLinksHandler: WalletDeepLinksHandler,
|
||||
private val walletNameMigrationUseCase: WalletNameMigrationUseCase,
|
||||
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
|
||||
private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase,
|
||||
private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase,
|
||||
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
|
||||
private fun subscribeToScreenBackgroundState() {
|
||||
screenLifecycleProvider.isBackgroundState
|
||||
|
|
@ -214,7 +215,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
refreshWalletJobHolder.cancel()
|
||||
when {
|
||||
isBackground -> needToRefreshTimer()
|
||||
needToRefreshWallet && !isBackground -> triggerRefreshWallet()
|
||||
needToRefreshWallet && !isBackground -> triggerRefreshWalletQuotes()
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
|
@ -227,13 +228,15 @@ internal class WalletViewModel @Inject constructor(
|
|||
}.saveIn(refreshWalletJobHolder)
|
||||
}
|
||||
|
||||
private fun triggerRefreshWallet() {
|
||||
private fun triggerRefreshWalletQuotes() {
|
||||
needToRefreshWallet = false
|
||||
val state = stateHolder.uiState.value
|
||||
val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return
|
||||
wallet.pullToRefreshConfig.onRefresh.invoke(
|
||||
WalletPullToRefreshConfig.ShowRefreshState(false),
|
||||
)
|
||||
viewModelScope.launch {
|
||||
refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse {
|
||||
Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it")
|
||||
}
|
||||
}.saveIn(refreshWalletJobHolder)
|
||||
}
|
||||
|
||||
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.UnlockWallet -> unlockWallet(action)
|
||||
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))
|
||||
}
|
||||
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
|
||||
|
||||
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.settings.NeverToShowWalletsScrollPreview
|
||||
import com.tangem.domain.tokens.FetchCardTokenListUseCase
|
||||
|
|
@ -38,6 +40,7 @@ internal class WalletClickIntents @Inject constructor(
|
|||
private val fetchTokenListUseCase: FetchTokenListUseCase,
|
||||
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -118,7 +121,13 @@ internal class WalletClickIntents @Inject constructor(
|
|||
}
|
||||
|
||||
maybeFetchResult.onLeft {
|
||||
stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it))
|
||||
stateHolder.update(
|
||||
SetTokenListErrorTransformer(
|
||||
selectedWallet = userWallet,
|
||||
error = it,
|
||||
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
stateHolder.update(
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
|||
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
|
||||
import com.tangem.domain.tokens.FetchTokenListUseCase
|
||||
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.models.UnlockWalletsError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
|
||||
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.MainScreen
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
|
||||
|
|
@ -222,6 +222,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCloseSwapPromoClick() {
|
||||
analyticsEventHandler.send(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||
),
|
||||
)
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoWalletUseCase.neverToShow()
|
||||
}
|
||||
|
|
@ -229,10 +236,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onTravalaPromoClick(link: String?) {
|
||||
analyticsEventHandler.send(
|
||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = "Travala",
|
||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
|
||||
),
|
||||
)
|
||||
link?.let {
|
||||
|
|
@ -244,10 +251,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
|
||||
override fun onCloseTravalaPromoClick() {
|
||||
analyticsEventHandler.send(
|
||||
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
|
||||
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
|
||||
source = AnalyticsParam.ScreensSources.Main,
|
||||
programName = "Travala",
|
||||
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed,
|
||||
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
|
||||
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
|
||||
),
|
||||
)
|
||||
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