diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 62d47b8ec8..02e790d2c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,7 @@ ? = null private set - private val activities = WeakHashMap, Activity>() + private val activities = WeakHashMap, 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 62a0089954..edd4f38350 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() @@ -194,9 +200,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() 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 { + private fun createAppThemeModeFlow(): SharedFlow { 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(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 diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 6d94e69c9d..9a6577a66f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -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()) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index b9a831cb3d..8ba8e26539 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -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") diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt new file mode 100644 index 0000000000..a164e0dde1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -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"), + ), + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt new file mode 100644 index 0000000000..1393bfbf57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt @@ -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), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt index 6da93efeb4..9b326b64c7 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardSDKLoggerModule.kt @@ -33,6 +33,7 @@ internal object CardSDKLoggerModule { Log.Level.Network, Log.Level.Error, Log.Level.Biometric, + Log.Level.Info, ) return TangemCardSDKLogger( diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt similarity index 71% rename from data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt rename to app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt index c88f2dbfc2..118bcb16a7 100644 --- a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardSdkModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 1a6c5ff684..3d1b711ccf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 92b7c76800..6e2c244f20 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -398,4 +398,16 @@ internal object TokensDomainModule { stakingRepository = stakingRepository, ) } + + @Provides + @Singleton + fun provideRefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + ): RefreshMultiCurrencyWalletQuotesUseCase { + return RefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index f63f480485..966f9613e9 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -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 = either { - enterRequiredAccessCode(card) { + override suspend fun invoke(card: CardDTO): Either = 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 = either { - enterRequiredAccessCode(card) { - tangemSdkManager.resetBackupCard(cardNumber, userWalletId) - .doOnSuccess { Unit.right() } - .doOnFailure { raise(it.mapToDomainError()) } + ): Either = 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) { + 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 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt index b0d2d24119..d0f1c97dda 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/UserWalletIdPreflightReadFilter.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 52e9be2c6d..199b92bf71 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -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 { 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, + 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) { - 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 = state.value.userWallets, selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId, diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt index 16a0cb98a5..22bea5bf11 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt @@ -17,5 +17,5 @@ interface CustomTokenInteractor { /** Save token [customCurrency] */ @Throws(TangemError::class) - suspend fun saveToken(customCurrency: CustomCurrency) + suspend fun saveToken(customCurrency: CustomCurrency): Result } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt index de73abac25..ad210d7625 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt @@ -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 { + 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? { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index 3bae12b5a1..443a8af290 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -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().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, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt index 6bb0c880af..966af2bd6a 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidator.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index da59bdc238..df99426334 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -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 = emptyList() /** Screen state */ - var uiState by mutableStateOf(getInitialUiState()) - private set + val uiState: MutableStateFlow = 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 { - 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() .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() .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 { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 302a437624..b48655b7c7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -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() { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 981f7fe8a6..bde329b131 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index f2d048b261..837fd35328 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 2c77d5806e..8e7e583ecf 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 6b2e7e748b..491f7dfc81 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -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() } } else { - store.dispatchNavigationAction { popTo() } + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess + if (isLocked && userWalletsListManager.hasUserWallets) { + store.dispatchNavigationAction { popTo() } + } else { + store.dispatchNavigationAction { popTo() } + } } } @@ -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") + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index ad934cdebd..203a2f983f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 = { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index 5aca214e93..7fb08b3681 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -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") diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index b77078a426..c7c73612f8 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -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) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index acbd56ec03..bda2f0e17f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -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, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index a269f45c23..dc786b02de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -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( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 47ad806d82..722573fc37 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -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 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c7913d2da6..015132e02d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -235,6 +235,7 @@ Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. Статус обмена Требуется верификация + Ожидание хеша транзакции Список токенов в вашем кошельке Получение наилучших курсов... Плавающая ставка @@ -248,6 +249,7 @@ Доступно с %s Недоступно для этой пары Требуется разрешение + Рекомендовано Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s @@ -584,6 +586,8 @@ Встречайте Tangem Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах Поддержка Web 3.0 + Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. + Новый провайдер обмена! В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя В сумму включена комиссия провайдера сервиса. Комиссии diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 28d7f3aefc..b38d48131f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status Verification required + Awaiting transaction hash List of all tokens added to your wallet Fetching best rates... Floating rate @@ -251,6 +252,7 @@ Available from %s Unavailable for this pair Permission Required + Recommended Terms of Use No tokens found. Please try another request ID: %s @@ -610,6 +612,8 @@ Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services Web 3.0 Compatible + Exchange more tokens at better rates directly in your wallet. + New Swap Provider Available! The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. The amount includes the service provider\'s fee. Fees diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt new file mode 100644 index 0000000000..9d6d403946 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/OkxPromoNotification.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 269ec76fd1..33a8bd032e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 5ff9fc7e8d..2a78787608 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -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 } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt index 51d450ed02..48565c82d7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt @@ -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. diff --git a/core/ui/src/main/res/drawable/img_okx_dex_logo.xml b/core/ui/src/main/res/drawable/img_okx_dex_logo.xml new file mode 100644 index 0000000000..3a4ba80759 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_okx_dex_logo.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt deleted file mode 100644 index 212ff3a8bf..0000000000 --- a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt new file mode 100644 index 0000000000..a5654ef6ec --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkOwner.kt @@ -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) +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt index 0d0b88fa41..71c34a1266 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt @@ -7,7 +7,7 @@ import com.tangem.TangemSdk * [REDACTED_AUTHOR] */ -internal interface CardSdkProvider { +interface CardSdkProvider { /** CardSDK instance */ val sdk: TangemSdk diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt deleted file mode 100644 index d8427a907f..0000000000 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ /dev/null @@ -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"), - ), - ), - ) - } -} \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index a92a4f5eb9..e48f0e6933 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -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" } } \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt index 80ba44157d..ce6d3fa236 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPromoSettingsRepository.kt @@ -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 { - 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 { - 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, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 5705476197..59ddeeb01d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -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 diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 4c9d33c1d9..7951e83c17 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -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 { @@ -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) } } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index a3c7834ed9..83f30079f3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -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 + } } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 1dee0062ce..f6c7ade8b4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -51,6 +51,19 @@ internal class DefaultQuotesRepository( .flowOn(dispatchers.io) } + override suspend fun fetchQuotes(currenciesIds: Set) { + withContext(dispatchers.io) { + val selectedAppCurrency = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + 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, refresh: Boolean): Set { return withContext(dispatchers.io) { val selectedAppCurrency = requireNotNull( diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index fb35baa42f..7d447a5359 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -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 + } } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt index 7db8690e89..eb3c61288d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/models/ResetCardError.kt @@ -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() } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt index 9be30e9699..6204c49430 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -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 = Flow> * 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 @PublishedApi internal constructor( private val raise: LceRaise, - private val scope: ProducerScope>, + private val producerScope: ProducerScope>, private val ifLoading: suspend LceFlowScope.(C?) -> Unit, -) : Raise, CoroutineScope by scope { +) : Raise, 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 @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) { - scope.send(value) + if (producerScope.isClosedForSend) return + + producerScope.send(value) } } @@ -94,7 +114,7 @@ fun lceFlow( lce { val scope = LceFlowScope( raise = this@lce, - scope = this@channelFlow, + producerScope = this@channelFlow, ifLoading = ifLoading, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 0a0a7eee52..55cc073069 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -54,6 +54,10 @@ interface CardTypesResolver { fun isVoltInuWallet(): Boolean + fun isVividWallet(): Boolean + + fun isPastelWallet(): Boolean + fun isWhiteWallet(): Boolean fun isWallet2(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index f43573bf37..82208040e8 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -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" } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index efbedfc151..68f4060640 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -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, ) { - 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 { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt index cc39985633..9e9156c366 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -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 = 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, + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 9febf735fe..e8c6f0b7a8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -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 { + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { 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 = either { + ): Either = 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, - ): Either = either { + ): Either = 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.refreshUpdatedNetworks( + private suspend fun Raise.refreshUpdatedNetworks( userWalletId: UserWalletId, currenciesToAdd: List, existingCurrencies: List, @@ -114,7 +113,7 @@ class AddCryptoCurrenciesUseCase( ) }, ) { - raise(AddCurrencyError.DataError(it)) + raise(it) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 1e64511bf5..29592489a4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -87,7 +87,7 @@ class GetCurrencyWarningsUseCase( ): Flow { 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(), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt new file mode 100644 index 0000000000..e108b21819 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -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 { + 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> { + return either { + catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, false) }, + catch = { raise(it) }, + ) + } + } + + private suspend fun fetchQuotes(currenciesIds: Set) { + catch( + block = { quotesRepository.fetchQuotes(currenciesIds) }, + catch = { /* Ignore error */ }, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt deleted file mode 100644 index 87255e52f7..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/AddCurrencyError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.tokens.error - -sealed class AddCurrencyError { - - data class DataError(val cause: Throwable) : AddCurrencyError() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt new file mode 100644 index 0000000000..f5601eb6db --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.tokens.error + +sealed class QuotesError { + data class DataError(val cause: Throwable) : QuotesError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt index ed42ab1cfc..51ba0a57f9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/PromoRepository.kt @@ -7,4 +7,6 @@ interface PromoRepository { suspend fun getChangellyPromoBanner(): PromoBanner? suspend fun getTravalaPromoBanner(): PromoBanner? + + suspend fun getOkxPromoBanner(): PromoBanner? } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 64a0d29a12..4ebd284421 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -31,4 +31,6 @@ interface QuotesRepository { suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? + + suspend fun fetchQuotes(currenciesIds: Set) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index ae960f191a..3d45f98879 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -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) {} } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index ecb5d93de7..7461386ac3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -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" } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 231c029e8f..48899c5eb3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -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() { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 7f0cb473cc..446343bfeb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -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 = MutableStateFlow( + value = stateFactory.getInitialState(), + ) private var userWallet: UserWallet by Delegates.notNull() private var userWallets: List = 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? { 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, ) { - 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) { - 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 diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 5bb5079f95..ec5a88e56e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -45,6 +45,7 @@ class SwapPairInfoConverter : Converter ExchangeProviderTypeDomain.DEX ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX + ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE } } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index a422c02284..c52f37ce6c 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -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, } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index 3eb907948a..d771aa4ecb 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -26,6 +26,7 @@ data class SavedSwapTransactionListModelInner( val transactions: List, ) +// TODO refactor to use separate models to store data class SavedSwapTransactionModel( val txId: String, val timestamp: Long, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index 9df61116fa..9e82c4a39f 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -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"), } /** diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 68521fe254..1e04778c76 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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() } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 3e42fef158..3f20bc09bc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 2c8c11fb70..fd4f8c9775 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -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") + } + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index d139a7b75f..5fb9348584 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index d94153492f..315c7a064a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -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) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a4a31d2962..2af78f3dfd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 68735250d9..335a399520 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -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"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index e8b04b03fc..7ae3edc6a2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 398a248ec5..6237a44e56 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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" } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index a69442cd51..878dc66047 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 950f24a218..a09065820b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -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), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 5f8bf6995f..a0231a352a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -162,6 +162,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { 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 { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 2c71568659..95bd0ed963 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 1df5e537c1..a1a7c595a5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -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, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 4bc8e7fb37..e77952c869 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -163,6 +163,7 @@ internal class ExchangeStatusFactory( return when (status) { ExchangeStatus.New, ExchangeStatus.Waiting, + ExchangeStatus.WaitingTxHash, ExchangeStatus.Sending, ExchangeStatus.Confirming, ExchangeStatus.Exchanging, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 9553f54131..bccaa00b68 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index f39a38511b..de6ddf8b1b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent { data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } - - sealed class Promotion( - event: String, - params: Map = 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") - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 59d4331387..398372e208 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -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 } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index a2adf6b292..b0513c7656 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -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> { 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.addTravalaPromoNotification( + private fun MutableList.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, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt index ab7e236835..a78ded4bf0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt @@ -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 { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index a5691979c9..722f7de8f5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -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) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index af25a1257b..713d704e94 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -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) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index c34e986c29..4a3247b5e1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -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, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 046b5fd498..fd70fb3cfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -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(), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index c9bd3afc92..f074661604 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -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) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 6b783e0eb0..08a341ebb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -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 { - NotificationWithBackground( + OkxPromoNotification( config = it.config, modifier = modifier.animateItemPlacement(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index c5aa3e94ac..b88fbcf1f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -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 -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 712975635b..ec3f2d992d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index c27292d883..e90a55faac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -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) { diff --git a/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp new file mode 100644 index 0000000000..1b024df94d Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_pastel_cards3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp new file mode 100644 index 0000000000..ecc36428ee Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_vivid_cards3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8cecca280d..4742ffb2c9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-690" +tangemBlockchainSdk = "develop-699" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-366" +tangemCardSdk = "develop-370" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^