diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ff2af8354..7817290795 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,13 +33,13 @@ configurations.all { dependencies { implementation(files("libs/walletconnect-1.5.6.aar")) - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:core")) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.core) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(project(":domain:wallets")) - implementation(project(":domain:wallets:models")) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) @@ -48,21 +48,24 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) - implementation(project(":common")) - implementation(project(":core:analytics")) + implementation(projects.common) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:navigation")) - implementation(project(":core:featuretoggles")) - implementation(project(":core:res")) - implementation(project(":core:ui")) - implementation(project(":core:datasource")) - implementation(project(":core:utils")) - implementation(project(":libs:crypto")) - implementation(project(":libs:auth")) + implementation(projects.core.navigation) + implementation(projects.core.featuretoggles) + implementation(projects.core.res) + implementation(projects.core.ui) + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.libs.crypto) + implementation(projects.libs.auth) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) + implementation(projects.data.balanceHiding) implementation(projects.data.card) implementation(projects.data.common) implementation(projects.data.settings) @@ -72,22 +75,23 @@ dependencies { implementation(projects.data.wallets) /** Features */ - implementation(project(":features:onboarding")) - implementation(project(":features:learn2earn:api")) - implementation(project(":features:learn2earn:impl")) - implementation(project(":features:referral:presentation")) - implementation(project(":features:referral:domain")) - implementation(project(":features:referral:data")) - implementation(project(":features:swap:api")) - implementation(project(":features:swap:presentation")) - implementation(project(":features:swap:domain")) - implementation(project(":features:swap:data")) - implementation(project(":features:tester:api")) - implementation(project(":features:tester:impl")) - implementation(project(":features:wallet:api")) - implementation(project(":features:wallet:impl")) + implementation(projects.features.onboarding) + implementation(projects.features.learn2earn.api) + implementation(projects.features.learn2earn.impl) + implementation(projects.features.referral.presentation) + implementation(projects.features.referral.domain) + implementation(projects.features.referral.data) + implementation(projects.features.swap.api) + implementation(projects.features.swap.presentation) + implementation(projects.features.swap.domain) + implementation(projects.features.swap.data) + implementation(projects.features.tester.api) + implementation(projects.features.tester.impl) + implementation(projects.features.wallet.api) + implementation(projects.features.wallet.impl) implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) + implementation(projects.features.send.api) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -106,6 +110,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.constraintLayout) implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index e10caf1e7f..dd90c8b04c 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -517,6 +517,17 @@ "networkId": "aleph-zero/test" } ] + }, + { + "id": "near", + "symbol": "NEAR", + "name": "NEAR", + "networks": + [ + { + "networkId": "near-protocol/test" + } + ] } ] } diff --git a/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt b/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt deleted file mode 100644 index 19cd307271..0000000000 --- a/app/src/main/java/com/tangem/tap/GlobalSettingsState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap - -import com.tangem.domain.apptheme.model.AppThemeMode - -internal sealed class GlobalSettingsState { - - object Loading : GlobalSettingsState() - - data class Content( - val appThemeMode: AppThemeMode, - ) : GlobalSettingsState() -} \ 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 efa00b067f..2b66b30ca7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,19 +1,26 @@ package com.tangem.tap +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.ActivityInfo +import android.content.res.Configuration import android.os.Bundle import android.view.View -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope +import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -27,15 +34,11 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.shop.googlepay.GooglePayService import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient import com.tangem.tap.domain.TangemSdkManager -import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation -import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -44,19 +47,15 @@ import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.features.welcome.redux.WelcomeAction +import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -112,8 +111,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var walletConnectInteractor: WalletConnectInteractor - private val viewModel: MainViewModel by viewModels() - private var isInitializing: Boolean = true + private lateinit var appThemeModeFlow: SharedFlow // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() @@ -125,34 +123,31 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - val splashScreen = installSplashScreen() + installSplashScreen() + installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation super.onCreate(savedInstanceState) - cardSdkLifecycleObserver.onCreate(context = this) - - bootstrapMainStateUpdates() - - splashScreen.setKeepOnScreenCondition { isInitializing } + installActivityDependencies() + observeAppThemeModeUpdates() setContentView(R.layout.activity_main) - systemActions() + initContent() + checkGooglePayAvailability() + } + + private fun installActivityDependencies() { store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) + cardSdkLifecycleObserver.onCreate(context = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer(owner = this) - initUserWalletsListManager() initIntentHandlers() - store.dispatch( - ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this), - ), - ) store.dispatch( DaggerGraphAction.SetActivityDependencies( testerRouter = testerRouter, @@ -165,6 +160,59 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } + private fun installAppTheme() { + appThemeModeFlow = createAppThemeModeFlow() + val mode = runBlocking { appThemeModeFlow.filterNotNull().first() } + + updateAppTheme(mode) + } + + private fun observeAppThemeModeUpdates() { + appThemeModeFlow + .filterNotNull() + .flowWithLifecycle(lifecycle) + .onEach(::updateAppTheme) + .launchIn(lifecycleScope) + } + + @SuppressLint("SourceLockedOrientationActivity") + private fun initContent() { + WindowCompat.setDecorFitsSystemWindows(window, false) + + supportFragmentManager.registerFragmentLifecycleCallbacks( + NavBarInsetsFragmentLifecycleCallback(), + true, + ) + + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + + private fun checkGooglePayAvailability() { + store.dispatch( + ShopAction.CheckIfGooglePayAvailable( + GooglePayService(createPaymentsClient(this), this), + ), + ) + } + + private fun createAppThemeModeFlow(): SharedFlow { + val tapApplication = application as TapApplication + val featureToggle = tapApplication.darkThemeFeatureToggle + + return if (featureToggle.isDarkThemeEnabled) { + tapApplication.getAppThemeModeUseCase() + .map { maybeMode -> + maybeMode.getOrElse { AppThemeMode.DEFAULT } + } + .shareIn( + scope = lifecycleScope + Dispatchers.IO, + started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), + ) + } else { + MutableStateFlow(AppThemeMode.FORCE_LIGHT) + } + } + override fun onStart() { super.onStart() dialogManager.onStart(this) @@ -175,7 +223,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) - navigateToInitialScreenIfNeededOnResume(intent) + navigateToInitialScreenIfNeeded(intent) } override fun onStop() { @@ -193,53 +241,32 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun initIntentHandlers() { val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } - intentProcessor.addHandler( - BackgroundScanIntentHandler( - hasSavedWalletsProvider, - lifecycleScope, - ), - ) + intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) intentProcessor.addHandler(BuyCurrencyIntentHandler()) intentProcessor.addHandler(SellCurrencyIntentHandler()) } - private fun initUserWalletsListManager() { - val manager = if (preferencesStorage.shouldSaveUserWallets) { - UserWalletsListManager.provideBiometricImplementation( - context = applicationContext, - tangemSdkManager = tangemSdkManager, - ) - } else { - UserWalletsListManager.provideRuntimeImplementation() + 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 + AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM } - store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + + setDefaultNightMode(mode) } - private fun bootstrapMainStateUpdates() { - viewModel.state - .onEach { state -> - isInitializing = state is GlobalSettingsState.Loading - - when (state) { - is GlobalSettingsState.Content -> { - MutableAppThemeModeHolder.value = state.appThemeMode - } - is GlobalSettingsState.Loading -> Unit - } - } - .launchIn(lifecycleScope) - } - - private fun systemActions() { - WindowCompat.setDecorFitsSystemWindows(window, false) - - supportFragmentManager.registerFragmentLifecycleCallbacks( - NavBarInsetsFragmentLifecycleCallback(), - true, - ) - - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + private fun isDarkTheme(): Boolean { + return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> true + Configuration.UI_MODE_NIGHT_NO -> false + Configuration.UI_MODE_NIGHT_UNDEFINED -> false + else -> false + } } override fun onNewIntent(intent: Intent?) { @@ -303,43 +330,40 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lockUserWalletsTimer?.restart() } - private fun navigateToInitialScreenIfNeededOnResume(intentWhichStartedActivity: Intent?) { + private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted val isShopNotOpened = store.state.shopState.total != null + when { !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> { - navigateToInitialScreenOnResume(intentWhichStartedActivity) + navigateToInitialScreen(intentWhichStartedActivity) } backStackIsEmpty -> { - navigateToInitialScreenOnResume(intentWhichStartedActivity) + navigateToInitialScreen(intentWhichStartedActivity) } else -> Unit } } - private fun navigateToInitialScreenOnResume(intentWhichStartedActivity: Intent?) { + private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome)) - store.dispatchOnMain(WelcomeAction.SetInitialIntent(intentWhichStartedActivity)) - lifecycleScope.launch { - val handler = BackgroundScanIntentHandler( - hasSavedUserWalletsProvider = { true }, - lifecycleCoroutineScope = lifecycleScope, - ) - val isBackgroundScanHandled = handler.handleIntent(intentWhichStartedActivity) - val hasNotIncompletedBackup = !backupService.hasIncompletedBackup - if (!isBackgroundScanHandled && hasNotIncompletedBackup) { - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics) - } - } + store.dispatch( + NavigationAction.NavigateTo( + screen = AppScreen.Welcome, + bundle = intentWhichStartedActivity?.let { + bundleOf(WelcomeFragment.INITIAL_INTENT_KEY to it) + }, + ), + ) } else { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) lifecycleScope.launch { intentProcessor.handleIntent(intentWhichStartedActivity) } } + store.dispatch(BackupAction.CheckForUnfinishedBackup) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainViewModel.kt b/app/src/main/java/com/tangem/tap/MainViewModel.kt deleted file mode 100644 index 2cb276000c..0000000000 --- a/app/src/main/java/com/tangem/tap/MainViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse -import com.tangem.domain.apptheme.GetAppThemeModeUseCase -import com.tangem.domain.apptheme.model.AppThemeMode -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -@HiltViewModel -internal class MainViewModel @Inject constructor( - private val getAppThemeModeUseCase: GetAppThemeModeUseCase, -) : ViewModel() { - - val state: StateFlow = createMainStateFlow() - - private fun createMainStateFlow(): StateFlow { - return getAppThemeModeUseCase() - .map { maybeMode -> - val mode = maybeMode.getOrElse { AppThemeMode.DEFAULT } - - GlobalSettingsState.Content(appThemeMode = mode) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000), - initialValue = GlobalSettingsState.Loading, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 8c4091a21f..2f6e54f690 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -22,13 +22,20 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.domain.DomainLayer +import com.tangem.datasource.local.token.UserTokensStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.GetAppThemeModeUseCase +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig +import com.tangem.domain.settings.repositories.AppRatingRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletManagersRepository -import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -48,9 +55,13 @@ import com.tangem.tap.common.redux.appReducer import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.shop.TangemShopService import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.domain.tokens.UserTokensStorageService import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation +import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation +import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation import com.tangem.tap.domain.walletStores.WalletStoresManager @@ -61,8 +72,11 @@ import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementa import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.features.details.DarkThemeFeatureToggle +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.runBlocking @@ -78,7 +92,8 @@ lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesDataSource lateinit var walletConnectRepository: WalletConnectRepository lateinit var shopService: TangemShopService -lateinit var userTokensRepository: UserTokensRepository +internal lateinit var userTokensRepository: UserTokensRepository +internal lateinit var derivationsFinder: DerivationsFinder private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() } private val walletManagersRepository by lazy { @@ -119,8 +134,9 @@ val totalFiatBalanceCalculator by lazy { } @HiltAndroidApp -class TapApplication : Application(), ImageLoaderFactory { +internal class TapApplication : Application(), ImageLoaderFactory { + // region Injected @Inject lateinit var appStateHolder: AppStateHolder @@ -154,9 +170,6 @@ class TapApplication : Application(), ImageLoaderFactory { // @Inject // lateinit var learn2earnInteractor: Learn2earnInteractor - @Inject - lateinit var tokenDetailsFeatureToggles: TokenDetailsFeatureToggles - @Inject lateinit var scanCardProcessor: ScanCardProcessor @@ -169,29 +182,41 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var walletManagersFacade: WalletManagersFacade + @Inject + lateinit var networksRepository: NetworksRepository + + @Inject + lateinit var currenciesRepository: CurrenciesRepository + + @Inject + lateinit var appThemeModeRepository: AppThemeModeRepository + + @Inject + lateinit var balanceHidingRepository: BalanceHidingRepository + + @Inject + lateinit var detailsFeatureToggles: DetailsFeatureToggles + + @Inject + lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle + + @Inject + lateinit var userTokensStore: UserTokensStore + + @Inject + lateinit var appRatingRepository: AppRatingRepository + + @Inject + lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase + + @Inject + lateinit var walletsRepository: WalletsRepository + // endregion Injected + override fun onCreate() { super.onCreate() - store = Store( - reducer = { action, state -> - appReducer(action, state, appStateHolder) - }, - middleware = AppState.getMiddleware(), - state = AppState( - daggerGraphState = DaggerGraphState( - assetReader = assetReader, - networkConnectionManager = networkConnectionManager, - customTokenFeatureToggles = customTokenFeatureToggles, - walletFeatureToggles = walletFeatureToggles, - walletConnectRepository = walletConnect2Repository, - walletConnectSessionsRepository = walletConnectSessionsRepository, - tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, - scanCardProcessor = scanCardProcessor, - appCurrencyRepository = appCurrencyRepository, - walletManagersFacade = walletManagersFacade, - ), - ), - ) + store = createReduxStore() if (BuildConfig.DEBUG) { Logger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) @@ -208,10 +233,19 @@ class TapApplication : Application(), ImageLoaderFactory { activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - DomainLayer.init() preferencesStorage = preferencesDataSource walletConnectRepository = WalletConnectRepository(this) + // TODO: Try to performance and user experience. + // [REDACTED_JIRA] + runBlocking { + walletsRepository.initialize() + initUserWalletsListManager() + featureTogglesManager.init() + appRatingRepository.initialize() + // learn2earnInteractor.init() + } + val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) initConfigManager(configLoader, ::initWithConfigDependency) initWarningMessagesManager() @@ -224,26 +258,53 @@ class TapApplication : Application(), ImageLoaderFactory { ) } + val userTokensStorageService = UserTokensStorageService.init(context = this) userTokensRepository = UserTokensRepository.init( - context = this, tangemTechService = store.state.domainNetworks.tangemTechService, networkConnectionManager = networkConnectionManager, + storageService = userTokensStorageService, + ) + derivationsFinder = DerivationsFinder( + legacyTokensStore = userTokensStorageService, + newTokensStore = userTokensStore, + walletFeatureToggles = walletFeatureToggles, + dispatchers = AppCoroutineDispatcherProvider(), ) appStateHolder.mainStore = store appStateHolder.userTokensRepository = userTokensRepository appStateHolder.walletStoresManager = walletStoresManager - // TODO: Try to performance and user experience. - // [REDACTED_JIRA] - runBlocking { - featureTogglesManager.init() - // learn2earnInteractor.init() - } - initTopUpController() walletConnect2Repository.init(projectId = configManager.config.walletConnectProjectId) } + private fun createReduxStore(): Store { + return Store( + reducer = { action, state -> appReducer(action, state, appStateHolder) }, + middleware = AppState.getMiddleware(), + state = AppState( + daggerGraphState = DaggerGraphState( + assetReader = assetReader, + networkConnectionManager = networkConnectionManager, + customTokenFeatureToggles = customTokenFeatureToggles, + walletFeatureToggles = walletFeatureToggles, + walletConnectRepository = walletConnect2Repository, + walletConnectSessionsRepository = walletConnectSessionsRepository, + scanCardProcessor = scanCardProcessor, + appCurrencyRepository = appCurrencyRepository, + walletManagersFacade = walletManagersFacade, + appStateHolder = appStateHolder, + networksRepository = networksRepository, + currenciesRepository = currenciesRepository, + appThemeModeRepository = appThemeModeRepository, + balanceHidingRepository = balanceHidingRepository, + detailsFeatureToggles = detailsFeatureToggles, + walletsRepository = walletsRepository, + ), + ), + ) + } + private fun initTopUpController() { val topUpController = TopUpController( scanResponseProvider = { @@ -303,14 +364,16 @@ class TapApplication : Application(), ImageLoaderFactory { foregroundActivityObserver: ForegroundActivityObserver, store: Store, ) { - fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { - appVersion = try { - // TODO don't use deprecated method - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - pInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - "x.y.z" + fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { + return AdditionalFeedbackInfo().apply { + appVersion = try { + // TODO don't use deprecated method + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + pInfo.versionName + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + "x.y.z" + } } } @@ -344,4 +407,14 @@ class TapApplication : Application(), ImageLoaderFactory { private fun initWarningMessagesManager() { store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager())) } + + private suspend fun initUserWalletsListManager() { + val manager = if (walletsRepository.shouldSaveUserWalletsSync()) { + UserWalletsListManager.provideBiometricImplementation(applicationContext) + } else { + UserWalletsListManager.provideRuntimeImplementation() + } + + store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt b/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt deleted file mode 100644 index b09112d39f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.common - -import timber.log.Timber - -class CompositionCounter( - val id: String, - count: Int = 0, -) { - var count: Int = count - private set - - fun increase(id: String): CompositionCounter { - if (this.id != id) return this - - count += 1 - return CompositionCounter(id, count) - } -} - -class CompositionLogger( - private val recomposeViewId: String, - private val tag: String = recomposeViewId, - private var turnOnForIds: List = listOf(recomposeViewId), -) { - val count: Int - get() = compositionCounter.count - - private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId) - - fun nextComposition() { - compositionCounter = compositionCounter.increase(recomposeViewId) - log("") - } - - fun log(message: String) { - if (!turnOnForIds.contains(recomposeViewId)) return - - Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt index b66ed0a3ad..a913befc98 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt @@ -4,6 +4,9 @@ import android.content.Context import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK +import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat import com.tangem.wallet.R @@ -15,6 +18,9 @@ class CustomTabsManager { .setNavigationBarColor(context.getColorCompat(R.color.toolbarColor)) .build(), ) + .setColorScheme( + if (MutableAppThemeModeHolder.isDarkThemeActive) COLOR_SCHEME_DARK else COLOR_SCHEME_LIGHT, + ) .build() customTabsIntent.launchUrl(context, Uri.parse(url)) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt new file mode 100644 index 0000000000..27f55538da --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultChangeCardAnalyticsContextUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.analytics.Analytics +import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.setContext + +internal class DefaultChangeCardAnalyticsContextUseCase : ChangeCardAnalyticsContextUseCase { + + override fun invoke(scanResponse: ScanResponse) { + Analytics.setContext(scanResponse) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index e5e4c697a6..e7e51bdd4c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -8,10 +8,7 @@ sealed class AnalyticsParam { class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol) class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) - class FiatCurrency( - fiatCurrency: com.tangem.tap.common.entities.FiatCurrency, - ) : CurrencyType(fiatCurrency.code) - + class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.code) class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) } @@ -136,9 +133,9 @@ sealed class AnalyticsParam { } sealed class WalletCreationType(val value: String) { - object PrivateKey : WalletCreationType("Private key") - object NewSeed : WalletCreationType("New seed") - object SeedImport : WalletCreationType("Seed import") + object PrivateKey : WalletCreationType(value = "Private Key") + object NewSeed : WalletCreationType(value = "New Seed") + object SeedImport : WalletCreationType(value = "Seed Import") } companion object Key { @@ -155,7 +152,8 @@ sealed class AnalyticsParam { const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" const val ERROR_KEY = "Error Key" - const val CREATION_TYPE = "Creation type" + const val CREATION_TYPE = "Creation Type" + const val SEED_PHRASE_LENGTH = "Seed Phrase Length" const val DAPP_NAME = "DApp Name" const val DAPP_URL = "DApp Url" const val METHOD_NAME = "Method Name" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index 2782ceac8f..ade60b95cf 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -32,14 +32,18 @@ sealed class Basic( batch: String, signInType: SignInType, walletsCount: String, + hasBackup: Boolean?, ) : Basic( event = "Signed in", - params = mapOf( - AnalyticsParam.CURRENCY to currency.value, - AnalyticsParam.BATCH to batch, - "Sign in type" to signInType.name, - "Wallets Count" to walletsCount, - ), + params = buildMap { + put(AnalyticsParam.CURRENCY, currency.value) + put(AnalyticsParam.BATCH, batch) + put("Sign in type", signInType.name) + put("Wallets Count", walletsCount) + if (hasBackup != null) { + put("Backuped", if (hasBackup) "Yes" else "No") + } + }, ) { enum class SignInType { Card, Biometric diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index a9432ad404..9088608413 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -23,9 +23,14 @@ sealed class Onboarding( class ButtonCreateWallet : CreateWallet("Button - Create Wallet") class WalletCreatedSuccessfully( creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey, + seedPhraseLength: Int? = null, ) : CreateWallet( event = "Wallet Created Successfully", - params = mapOf(AnalyticsParam.CREATION_TYPE to creationType.value), + params = buildMap { + put(AnalyticsParam.CREATION_TYPE, creationType.value) + + if (seedPhraseLength != null) put(AnalyticsParam.SEED_PHRASE_LENGTH, seedPhraseLength.toString()) + }, ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt index 3e961883b7..8270df641c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt @@ -12,6 +12,10 @@ sealed class Portfolio( ) : AnalyticsEvent("Portfolio", event, params) { class Refreshed : Portfolio("Refreshed") + class ButtonManageTokens : Portfolio("Button - Manage Tokens") + class TokenTapped : Portfolio("Token is Tapped") + + class OrganizeTokens : Portfolio("Button - Organize Tokens") } \ No newline at end of file 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 e71eda15ad..7208d9943a 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 @@ -22,6 +22,7 @@ sealed class Settings( class ButtonAppSettings : Settings(event = "Button - App Settings") class ButtonCreateBackup : Settings(event = "Button - Create Backup") class ButtonWalletConnect : Settings(event = "Button - Wallet Connect") + object ScanNewCard : Settings(event = "Button - Scan New Card") class ButtonSocialNetwork(network: SocialNetwork) : Settings( event = "Button - Social Network", @@ -78,5 +79,10 @@ sealed class Settings( ) object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication") + + class MainCurrencyChanged(currencyType: String) : MainScreen( + event = "Main Currency Changed", + params = mapOf("Currency Type" to currencyType), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt b/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt index c1b0f942c5..9dec177e01 100644 --- a/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt +++ b/app/src/main/java/com/tangem/tap/common/apptheme/MutableAppThemeModeHolder.kt @@ -14,4 +14,6 @@ internal object MutableAppThemeModeHolder : AppThemeModeHolder { appThemeMode.value = value } get() = appThemeMode.value + + var isDarkThemeActive: Boolean = false } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt deleted file mode 100644 index 59cf01d88c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldColors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.common.module.ModuleError -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.CompositionLogger -import com.tangem.tap.common.compose.extensions.stringResourceDefault -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun OutlinedTextFieldWidget( - fieldData: Field.Data, - labelId: Int? = null, - label: String = "", - placeholderId: Int? = null, - placeholder: String = "", - trailingIcon: @Composable (() -> Unit)? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - errorConverter: ModuleMessageConverter? = null, - debounceTextChanges: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - onTextChange: (String) -> Unit, -) { - if (!isVisible) return - - Column(modifier = Modifier.animateContentSize()) { - OutlinedProgressTextField( - fieldData = fieldData, - label = stringResourceDefault(labelId, label), - placeholder = stringResourceDefault(placeholderId, placeholder), - trailingIcon = trailingIcon, - isEnabled = isEnabled, - isLoading = isLoading, - error = error, - debounce = debounceTextChanges, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - onTextChange = onTextChange, - ) - errorConverter?.let { AnimatedErrorView(errorConverter = it, error = error) } - } -} - -@Suppress("LongMethod", "NestedBlockDepth", "MagicNumber", "MaxLineLength") -@Composable -private fun OutlinedProgressTextField( - fieldData: Field.Data, - label: String = "", - placeholder: String = "", - isEnabled: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - debounce: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - colors: TextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - trailingIcon: @Composable (() -> Unit)? = null, - onTextChange: (String) -> Unit, -) { - val logger = remember { - CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена")) - } - logger.nextComposition() - - val textValueState = remember { mutableStateOf(fieldData.value) } - val textDebouncer = valueDebouncerAsState( - initialValue = fieldData.value, - debounce = debounce, - onEmitValueReceive = { - logger.log("DEBOUNCER: onEmitValueReceived: [$it]") - logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]") - textValueState.value = it - }, - onValueChange = { - logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])") - onTextChange(it) - }, - ) - - logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]") - logger.log("RECOMPOSE --data: fieldData.value: [$fieldData]") - logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]") - logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]") - logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]") - - if (!fieldData.isUserInput) { - // initial value is not from an user - val isNotUserInput = "-- IS NOT USER INPUT" - logger.log("recompose $isNotUserInput") - if (textValueState.value == fieldData.value) { - logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле") - } else { - logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле") - if (textDebouncer.emittedValue != textDebouncer.debounced || textDebouncer.emitsCountBeforeDebounce > 0) { - logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE") - } else { - logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные") - if (textValueState.value != textDebouncer.emittedValue || - textValueState.value != textDebouncer.debounced - ) { - logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer") - if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) { - logger.log( - "$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } else { - logger.log( - "$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } else { - logger.log( - "$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } - } - } - logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]") - - Box { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(), - value = textValueState.value, - onValueChange = { - logger.log("WIDGET: textDebouncer.emmit([$it])") - textDebouncer.emmit(it) - }, - keyboardOptions = keyboardOptions, - label = { - Text( - text = label, - style = TangemTheme.typography.caption, - color = colors.labelColor( - enabled = isEnabled, - error = error != null, - interactionSource = interactionSource, - ).value, - ) - }, - placeholder = { - Text( - text = placeholder, - style = TangemTheme.typography.body1, - color = colors.placeholderColor(enabled = isEnabled).value, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - trailingIcon = trailingIcon, - singleLine = true, - enabled = isEnabled, - isError = error != null, - visualTransformation = visualTransformation, - colors = colors, - interactionSource = interactionSource, - ) - AnimatedVisibility( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp), - visible = isLoading, - ) { - LinearProgressIndicator( - color = TangemTheme.colors.icon.primary1, - ) - } - } -} - -@Composable -private fun AnimatedErrorView(errorConverter: ModuleMessageConverter, error: ModuleError? = null) { - AnimatedVisibility( - visible = error != null, - enter = fadeIn() + slideInVertically(), - exit = slideOutVertically() + fadeOut(), - ) { - error?.let { - ErrorView( - text = errorConverter.convert(it).message, - style = TextStyle(fontSize = 14.sp), - ) - } - } -} - -@Preview -@Composable -private fun OutlinedTextFieldWithErrorTest() { - val context = LocalContext.current - val converter = remember { ModuleMessageConverter(context) } - - class SimpleError( - override val code: Int = 1, - override val message: String = "Error message", - override val data: Any? = null, - ) : ModuleError() - - val modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - Column { - OutlinedTextFieldWidget( - fieldData = Field.Data("", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - isLoading = true, - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = SimpleError(), - errorConverter = converter, - ) {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt deleted file mode 100644 index d5e5168be3..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.sp - -/** -[REDACTED_AUTHOR] - * Compose views are not typically used as a main or base view. - */ - -@Composable -fun TitleSubtitle(title: String, subtitle: String) { - Column { - Text(text = title) - Text( - text = subtitle, - fontSize = 12.sp, - color = Color.Gray, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt deleted file mode 100644 index 5b8da92e24..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import android.content.res.Resources -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String { - val resources = LocalContext.current.resources - return try { - resources.getString(requireNotNull(id)) - } catch (ex: Resources.NotFoundException) { - default - } catch (ex: IllegalArgumentException) { - default - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 88a8e16ed6..f1daaf34f6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -47,6 +47,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color Blockchain.Chia, Blockchain.ChiaTestnet -> R.drawable.ic_chia_no_color + Blockchain.Near, Blockchain.NearTestnet -> R.drawable.ic_near_no_color else -> R.drawable.ic_tangem_logo } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 9119734bfd..5ba15310c9 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -6,7 +6,8 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.FragmentShareTransition import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment +import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment @@ -33,7 +34,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R import timber.log.Timber -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment fun FragmentActivity.openFragment( screen: AppScreen, @@ -155,21 +155,10 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> TokensListFragment() - - AppScreen.AddCustomToken -> { - val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::customTokenFeatureToggles, - ) - if (featureToggles.isRedesignedScreenEnabled) { - RedesignedAddCustomTokenFragment() - } else { - AddCustomTokenFragment() - } - } - + AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::tokenDetailsFeatureToggles, + getDependency = DaggerGraphState::walletFeatureToggles, ) if (featureToggles.isRedesignedScreenEnabled) { store.state.daggerGraphState @@ -186,5 +175,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.Welcome -> WelcomeFragment() AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment() + AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt b/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt deleted file mode 100644 index 3dd4357bba..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.common.extensions - -/** -[REDACTED_AUTHOR] - */ -typealias ValueCallback = (T) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 1533313216..096f5c96bb 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -1,12 +1,12 @@ package com.tangem.tap.common.extensions -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.common.services.Result import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.tap.common.TestActions +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.domain.TapError import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel @@ -71,6 +71,7 @@ fun WalletManager.getTopUpUrl(): String? { cryptoCurrencyName = wallet.blockchain.currency, fiatCurrencyName = globalState.appCurrency.code, walletAddress = defaultAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, ) } @@ -79,12 +80,4 @@ fun WalletManager?.getAddressData(): WalletDataModel.AddressData? { val addressDataList = wallet.createAddressesData() return if (addressDataList.isEmpty()) null else addressDataList[0] -} - -fun WalletManager.Companion.stub(): T { - val wallet = Wallet(Blockchain.Unknown, setOf(), Wallet.PublicKey(byteArrayOf(), null), setOf()) - return object : WalletManager(wallet) { - override val currentHost: String = "" - override suspend fun update() {} - } as T } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 947691b26f..e1d67df755 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -7,8 +7,10 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.common.extensions.stripZeroPlainString +import kotlinx.coroutines.flow.* class AdditionalFeedbackInfo { + class EmailWalletInfo( var blockchain: Blockchain = Blockchain.Unknown, var derivationPath: String = "", @@ -46,6 +48,7 @@ class AdditionalFeedbackInfo { private val Address.name: String get() = type.javaClass.simpleName + @Deprecated("Don't use it directly") fun setCardInfo(data: ScanResponse) { cardId = data.card.cardId cardBlockchain = data.walletData?.blockchain ?: "" @@ -55,6 +58,7 @@ class AdditionalFeedbackInfo { userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: "" } + @Deprecated("Don't use it directly") fun setWalletsInfo(walletManagers: List) { walletsInfo.clear() tokens.clear() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 19248b08b9..7b9b9f2bd4 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -6,6 +6,7 @@ import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.NetworkServices import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.common.redux.navigation.navigationMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState @@ -68,7 +69,7 @@ data class AppState( val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { - val domainState: DomainState + private val domainState: DomainState get() = domainStore.state val domainNetworks: NetworkServices @@ -107,6 +108,7 @@ data class AppState( AccessCodeRequestPolicyMiddleware().middleware, SignInMiddleware.middleware, DaggerGraphMiddleware.daggerGraphMiddleware, + LegacyMiddleware.legacyMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 2d75cf1723..15227361e9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,11 +1,10 @@ package com.tangem.tap.common.redux.global -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemError import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -48,13 +47,6 @@ sealed class GlobalAction : Action { object Stop : Onboarding() } - data class ScanCard( - val additionalBlockchainsToDerive: Collection? = null, - val onSuccess: ((ScanResponse) -> Unit)? = null, - val onFailure: ((TangemError) -> Unit)? = null, - val messageResId: Int? = null, - ) : GlobalAction() - object ScanFailsCounter { data class ChooseBehavior(val result: CompletionResult) : GlobalAction() object Reset : GlobalAction() @@ -103,4 +95,5 @@ sealed class GlobalAction : Action { } data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() + data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 176841c442..68bc51a87c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard +import com.tangem.core.analytics.Analytics import com.tangem.datasource.config.models.Config import com.tangem.domain.common.LogConfig -import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.* +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDialogShow @@ -16,7 +16,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.exchangeServices.BuyExchangeService @@ -26,8 +25,13 @@ import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService +import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState -import kotlinx.coroutines.flow.firstOrNull +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.walletCurrenciesManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -66,7 +70,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } is GlobalAction.RestoreAppCurrency -> { - if (store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles).isRedesignedScreenEnabled) { + val daggerGraphState = store.state.daggerGraphState + val walletFeatureToggles = daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + val detailsFeatureToggles = daggerGraphState.get(DaggerGraphState::detailsFeatureToggles) + + if (walletFeatureToggles.isRedesignedScreenEnabled || + detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled + ) { restoreAppCurrencyNew() } else { restoreAppCurrencyLegacy() @@ -75,10 +85,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.HideWarningMessage -> { store.state.globalState.warningManager?.let { if (it.hideWarning(action.warning)) { - if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) { - // TODO: No appropriate warningMessage identification. Make it better later - store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) - } + // if (WarningMessagesManager.isAlreadySignedHashesWarning()) { + // // TODO: No appropriate warningMessage identification. Make it better later + // store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) + // } store.dispatch(WalletAction.Warnings.Update) store.dispatch(SendAction.Warnings.Update) @@ -131,6 +141,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di sellService = makeSellExchangeService(config), primaryRules = CardExchangeRules(cardProvider), ) + // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) + store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } @@ -143,28 +155,6 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } scope.launch { exchangeManager.update() } } - is GlobalAction.ScanCard -> { - scope.launch { - tangemSdkManager.changeDisplayedCardIdNumbersCount(null) - val result = tangemSdkManager.scanProduct( - userTokensRepository = userTokensRepository, - additionalBlockchainsToDerive = action.additionalBlockchainsToDerive, - messageRes = action.messageResId, - ) - withMainContext { - store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) - when (result) { - is CompletionResult.Success -> { - tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data) - action.onSuccess?.invoke(result.data) - } - is CompletionResult.Failure -> { - action.onFailure?.invoke(result.error) - } - } - } - } - } is GlobalAction.FetchUserCountry -> { scope.launch { // TODO("After adding DI") get dependencies by DI @@ -186,6 +176,28 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.SetTopUpController -> { walletCurrenciesManager.addListener(action.topUpController) } + is GlobalAction.UpdateUserWalletsListManager -> { + /* + * If UserWalletsListManager's implementation is changed, + * then all selectedUserWallet's observers is became irrelevant + */ + action.manager.selectedUserWallet + .distinctUntilChanged() + .onEach { userWallet -> + Analytics.send(event = Basic.WalletOpened()) + + store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder -> + infoHolder.setCardInfo(data = userWallet.scanResponse) + + store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade) + .getAll(userWalletId = userWallet.walletId) + .onEach(infoHolder::setWalletsInfo) + .launchIn(scope) + } + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 0c90110fcb..2b282844d5 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -97,6 +97,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userWalletsListManager = action.manager, ) } + is GlobalAction.ChangeAppThemeMode -> globalState.copy( + appThemeMode = action.appThemeMode, + ) else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 1e5a3cfce0..f5a30ba673 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.redux.global import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -29,6 +30,7 @@ data class GlobalState( val userCountryCode: String? = null, val userWalletsListManager: UserWalletsListManager? = null, val topUpController: TopUpController? = null, + val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) : StateType typealias CryptoCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt new file mode 100644 index 0000000000..495a45f59b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.common.redux.legacy + +import com.tangem.domain.redux.LegacyAction +import com.tangem.tap.common.feedback.RateCanBeBetterEmail +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.store +import org.rekotlin.Middleware + +internal object LegacyMiddleware { + val legacyMiddleware: Middleware = { _, _ -> + { next -> + { action -> + when (action) { + is LegacyAction.SendEmailRateCanBeBetter -> { + store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail()) + } + } + next(action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt index 365f235f11..3cd171d04a 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.ui import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store import com.tangem.wallet.R @@ -39,7 +40,7 @@ object SimpleCancelableAlertDialog { secondaryButtonAction: () -> Unit = {}, context: Context, ): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(titleRes?.let { context.getString(it) } ?: title) setMessage(messageRes?.let { context.getString(it) } ?: message) setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() } diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index a894d7c4fe..2ff65f28a2 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -3,14 +3,19 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.exchange.RampStateManager import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository -import com.tangem.tap.userTokensRepository +import com.tangem.tap.network.exchangeServices.DefaultRampManager +import com.tangem.tap.proxy.AppStateHolder import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -35,9 +40,21 @@ internal object ActivityModule { return ScanCardUseCase( cardSdkConfigRepository = cardSdkConfigRepository, scanCardRepository = DefaultScanCardRepository( - userTokensRepository = userTokensRepository, tangemSdkManager = tangemSdkManager, ), ) } + + @Provides + @Singleton + fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { + return DefaultRampManager(appStateHolder.exchangeService) + } + + @Provides + @Singleton + @DelayedWork + fun provideActivityDelayedWorkCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.IO) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/Qualifiers.kt b/app/src/main/java/com/tangem/tap/di/Qualifiers.kt new file mode 100644 index 0000000000..df7fab7779 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/Qualifiers.kt @@ -0,0 +1,8 @@ +@file:Suppress("Filename") +package com.tangem.tap.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class DelayedWork \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt new file mode 100644 index 0000000000..34dbbd2e11 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.analytics + +import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase +import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AnalyticsModule { + + @Provides + @Singleton + fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase { + return DefaultChangeCardAnalyticsContextUseCase() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt index c69bca18d1..ab7bc24782 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt @@ -1,22 +1,34 @@ package com.tangem.tap.di.domain +import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) internal object AppCurrencyDomainModule { @Provides - @ViewModelScoped fun provideGetSelectedAppCurrencyUseCase( appCurrencyRepository: AppCurrencyRepository, ): GetSelectedAppCurrencyUseCase { return GetSelectedAppCurrencyUseCase(appCurrencyRepository) } + + @Provides + fun provideSelectAppCurrencyUseCase(appCurrencyRepository: AppCurrencyRepository): SelectAppCurrencyUseCase { + return SelectAppCurrencyUseCase(appCurrencyRepository) + } + + @Provides + fun provideGetAvailableCurrenciesUseCase( + appCurrencyRepository: AppCurrencyRepository, + ): GetAvailableCurrenciesUseCase { + return GetAvailableCurrenciesUseCase(appCurrencyRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt index b4378101b0..4b6fc45a17 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppThemeDomainModule.kt @@ -6,10 +6,10 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.components.SingletonComponent @Module -@InstallIn(ViewModelComponent::class) +@InstallIn(SingletonComponent::class) internal object AppThemeDomainModule { @Provides 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 a7f064da55..918b41ff04 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 @@ -5,6 +5,10 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.tap.domain.TangemSdkManager +import com.tangem.tap.domain.card.DefaultDerivePublicKeysUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -41,11 +45,29 @@ internal object CardDomainModule { @Provides @ViewModelScoped - fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase { - return GetCardWasScannedUseCase(cardRepository = cardRepository) + fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase { + return WasCardScannedUseCase(cardRepository = cardRepository) + } + + @Provides + @ViewModelScoped + fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase { + return SetCardWasScannedUseCase(cardRepository = cardRepository) } @Provides @ViewModelScoped fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) + + @Provides + @ViewModelScoped + fun provideDerivePublicKeysUseCase(tangemSdkManager: TangemSdkManager): DerivePublicKeysUseCase { + return DefaultDerivePublicKeysUseCase(tangemSdkManager = tangemSdkManager) + } + + @Provides + @ViewModelScoped + fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(walletStateHolder) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index eaf0da329c..afe7009baa 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -1,8 +1,11 @@ package com.tangem.tap.di.domain -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.balancehiding.DeviceFlipDetector +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import com.tangem.domain.settings.* +import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository @@ -18,8 +21,28 @@ internal object SettingsDomainModule { @Provides @ViewModelScoped - fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase { - return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository) + fun providesIsReadyToShowRatingUseCase(appRatingRepository: AppRatingRepository): IsReadyToShowRateAppUseCase { + return IsReadyToShowRateAppUseCase(appRatingRepository = appRatingRepository) + } + + @Provides + @ViewModelScoped + fun providesRemindToRateAppLaterUseCase(appRatingRepository: AppRatingRepository): RemindToRateAppLaterUseCase { + return RemindToRateAppLaterUseCase(appRatingRepository = appRatingRepository) + } + + @Provides + @ViewModelScoped + fun providesNeverToSuggestRateAppUseCase(appRatingRepository: AppRatingRepository): NeverToSuggestRateAppUseCase { + return NeverToSuggestRateAppUseCase(appRatingRepository = appRatingRepository) + } + + @Provides + @ViewModelScoped + fun providesSetWalletWithFundsFoundUseCase( + appRatingRepository: AppRatingRepository, + ): SetWalletWithFundsFoundUseCase { + return SetWalletWithFundsFoundUseCase(appRatingRepository = appRatingRepository) } @Provides @@ -37,4 +60,24 @@ internal object SettingsDomainModule { legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager), ) } + + @Provides + @ViewModelScoped + fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase { + return IsBalanceHiddenUseCase( + balanceHidingRepository = balanceHidingRepository, + ) + } + + @Provides + @ViewModelScoped + fun providesListenUseCase( + flipDetector: DeviceFlipDetector, + balanceHidingRepository: BalanceHidingRepository, + ): ListenToFlipsUseCase { + return ListenToFlipsUseCase( + flipDetector = flipDetector, + balanceHidingRepository = balanceHidingRepository, + ) + } } \ No newline at end of file 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 1a77f50c7b..e9a9087e06 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 @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -13,6 +16,7 @@ import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) +@Suppress("TooManyFunctions") internal object TokensDomainModule { @Provides @@ -25,6 +29,14 @@ internal object TokensDomainModule { return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) } + @Provides + @ViewModelScoped + fun provideFetchPendingTransactionsUseCase( + networksRepository: NetworksRepository, + ): FetchPendingTransactionsUseCase { + return FetchPendingTransactionsUseCase(networksRepository) + } + @Provides @ViewModelScoped fun provideGetTokenListUseCase( @@ -36,6 +48,17 @@ internal object TokensDomainModule { return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetCardTokensListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCardTokensListUseCase { + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideRemoveCurrencyUseCase( @@ -56,6 +79,24 @@ internal object TokensDomainModule { return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetCurrencyWarningsUseCase( + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCurrencyWarningsUseCase { + return GetCurrencyWarningsUseCase( + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + @Provides @ViewModelScoped fun provideGetPrimaryCurrencyUseCase( @@ -82,6 +123,22 @@ internal object TokensDomainModule { return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository) } + @Provides + @ViewModelScoped + fun provideFetchCardTokenListUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + ): FetchCardTokenListUseCase { + return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + } + + @Provides + @ViewModelScoped + fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { + return GetCryptoCurrencyUseCase(currenciesRepository) + } + @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( @@ -108,8 +165,60 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( + rampStateManager: RampStateManager, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { - return GetCryptoCurrencyActionsUseCase(dispatchers) + return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideGetCurrencyStatusByNetworkUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetNetworkCoinStatusUseCase { + return GetNetworkCoinStatusUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + + @Provides + @ViewModelScoped + fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) + } + + @Provides + @ViewModelScoped + fun provideIsCryptoCurrencyCoinCouldHideUseCase( + currenciesRepository: CurrenciesRepository, + ): IsCryptoCurrencyCoinCouldHideUseCase { + return IsCryptoCurrencyCoinCouldHideUseCase( + currenciesRepository = currenciesRepository, + ) + } + + @Provides + @ViewModelScoped + fun provideUpdateDelayedCurrencyStatusUseCase( + networksRepository: NetworksRepository, + ): UpdateDelayedNetworkStatusUseCase { + return UpdateDelayedNetworkStatusUseCase( + networksRepository = networksRepository, + ) + } + + @Provides + @ViewModelScoped + fun provideHasMissedAddressesCryptoCurrenciesUseCase( + currenciesRepository: CurrenciesRepository, + ): GetMissedAddressesCryptoCurrenciesUseCase { + return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt index 3ab36cd231..7ba9e05cff 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.tokens.* import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import dagger.Module @@ -25,4 +26,12 @@ internal object TxHistoryDomainModule { fun provideGetTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsUseCase { return GetTxHistoryItemsUseCase(repository = txHistoryRepository) } + + @Provides + @ViewModelScoped + fun providesGetExplorerTransactionUrlUseCase( + txHistoryRepository: TxHistoryRepository, + ): GetExplorerTransactionUrlUseCase { + return GetExplorerTransactionUrlUseCase(repository = txHistoryRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 8e3d763c56..3634bdc1b8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -20,6 +20,18 @@ internal object WalletsDomainModule { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase { + return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesGetSelectedWalletSyncUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase { + return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder) + } + @Provides @ViewModelScoped fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase { @@ -62,6 +74,14 @@ internal object WalletsDomainModule { return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder) } + @Provides + @ViewModelScoped + fun providesShouldSaveUserWalletsSyncUseCase( + walletsRepository: WalletsRepository, + ): ShouldSaveUserWalletsSyncUseCase { + return ShouldSaveUserWalletsSyncUseCase(walletsRepository = walletsRepository) + } + @Provides @ViewModelScoped fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase { diff --git a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt deleted file mode 100644 index 3d2a5764e2..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.tap.domain - -import com.tangem.common.services.Result -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.domain.common.ThrottlerWithValues -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import java.math.BigDecimal - -class RatesRepository( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) { - private val throttler = ThrottlerWithValues?>(60000) - - suspend fun loadFiatRate(currencyId: String, coinsList: List): Result = - withContext(dispatchers.io) { - // get and submit previous result of equivalents. - val throttledResult = coinsList.filter { throttler.isStillThrottled(it) }.map { - Pair(it, throttler.geValue(it)) - } - - val currenciesToUpdate = coinsList.filter { !throttler.isStillThrottled(it) } - val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct() - if (coinIds.isEmpty()) return@withContext handleFiatRatesResult(throttledResult.toMap()) - - runCatching { tangemTechApi.getRates(currencyId.lowercase(), coinIds.joinToString(",")) } - .onSuccess { response -> - val ratesResultList: Map> = response.rates.mapValues { - Result.Success(it.value.toBigDecimal()) - } - val updatedCurrencies = throttledResult.toMap().toMutableMap() - coinsList.forEach { currency -> - ratesResultList[currency.coinId]?.let { - updatedCurrencies[currency] = it - throttler.updateThrottlingTo(currency) - throttler.setValue(currency, it) - } - } - return@withContext handleFiatRatesResult(updatedCurrencies) - } - .onFailure { Result.Failure(it) } - - error("Unreachable code because runCatching must return result") - } - - private fun handleFiatRatesResult(rates: Map?>): Result.Success { - val success = mutableMapOf() - val failures = mutableMapOf() - - rates.mapNotNull { (currency, priceResult) -> - when (priceResult) { - is Result.Success -> success[currency] = priceResult.data - is Result.Failure -> failures[currency] = priceResult.error - else -> null - } - } - - return Result.Success(success to failures) - } - - fun clear() { - throttler.clear() - } -} - -typealias RatesResult = Pair, MutableMap> - -val RatesResult.loadedRates - get() = this.first - -val RatesResult.failedRates - get() = this.second \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index a16c860685..ea5bd2e3aa 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -5,12 +5,12 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk -import com.tangem.blockchain.common.Blockchain import com.tangem.common.* -import com.tangem.common.biometric.BiometricManager +import com.tangem.common.authentication.KeystoreManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.crypto.bip39.DefaultMnemonic @@ -26,12 +26,12 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.tap.common.analytics.events.Basic +import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask -import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine @@ -39,31 +39,38 @@ import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions") -class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources) { +class TangemSdkManager( + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val resources: Resources, +) { private val tangemSdk: TangemSdk get() = cardSdkConfigRepository.sdk private val userCodeRepository by lazy { UserCodeRepository( - biometricManager = tangemSdk.biometricManager, + keystoreManager = tangemSdk.keystoreManager, secureStorage = tangemSdk.secureStorage, ) } val canUseBiometry: Boolean - get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics + get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics val needEnrollBiometrics: Boolean - get() = tangemSdk.biometricManager.canEnrollBiometrics + get() = tangemSdk.authenticationManager.needEnrollBiometrics - val biometricManager: BiometricManager - get() = tangemSdk.biometricManager + val keystoreManager: KeystoreManager + get() = tangemSdk.keystoreManager + + val secureStorage: SecureStorage + get() = tangemSdk.secureStorage + + val userCodeRequestPolicy: UserCodeRequestPolicy + get() = tangemSdk.config.userCodeRequestPolicy suspend fun scanProduct( - userTokensRepository: UserTokensRepository, cardId: String? = null, - additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { @@ -71,8 +78,7 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit return runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, - userTokensRepository = userTokensRepository, - additionalBlockchainsToDerive = additionalBlockchainsToDerive, + derivationsFinder = derivationsFinder, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, ), cardId = cardId, @@ -141,7 +147,9 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { return runTaskAsyncReturnOnMain( - runnable = ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository), + runnable = ResetToFactorySettingsTask( + allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + ), cardId = cardId, initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), ) @@ -247,17 +255,8 @@ class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigReposit return resources.getString(stringResId, *formatArgs) } - fun setAccessCodeRequestPolicy(useBiometricsForAccessCode: Boolean) { - tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) { - UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) - } else { - UserCodeRequestPolicy.Default - } - } - - fun useBiometricsForAccessCode(): Boolean { - val policy = tangemSdk.config.userCodeRequestPolicy - return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode + fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) { + tangemSdk.config.userCodeRequestPolicy = policy } companion object { diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 6d23a8b78a..9edc4ee417 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -19,6 +19,7 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -28,11 +29,13 @@ import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import org.rekotlin.Store import timber.log.Timber class TapWalletManager( @@ -75,17 +78,34 @@ class TapWalletManager( withMainContext { // Order is important store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer())) - store.dispatch(WalletAction.UserWalletChanged(userWallet)) - store.dispatch(WalletAction.UpdateCanSaveUserWallets(preferencesStorage.shouldSaveUserWallets)) + store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet)) + store.dispatchWalletAction( + action = WalletAction.UpdateCanSaveUserWallets( + canSaveUserWallets = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .shouldSaveUserWalletsSync(), + ), + ) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) store.dispatch(WalletConnectAction.RestoreSessions(scanResponse)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) - store.dispatch(WalletAction.Warnings.CheckIfNeeded) + store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded) + } + + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + setupWalletConnectV2(userWallet) + loadData(userWallet = userWallet, refresh = refresh) + } + } + + private fun Store.dispatchWalletAction(action: WalletAction) { + val walletFeatureToggles = state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + dispatch(action = action) } - setupWalletConnectV2(userWallet) - loadData(userWallet, refresh) } private fun setupWalletConnectV2(userWallet: UserWallet) { @@ -142,21 +162,31 @@ class TapWalletManager( } } - private fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List { - return store.state.walletState.walletManagers - .mapNotNull { - val wallet = it.wallet - val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), + private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor): List { + val walletManagerToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + val walletManagers = if (walletManagerToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() + walletManagerFacade.getStoredWalletManagers(userWallet.walletId) + } else { + store.state.walletState.walletManagers + } + + return walletManagers.mapNotNull { + val wallet = it.wallet + val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } } + } } fun updateConfigManager(data: ScanResponse) { diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt new file mode 100644 index 0000000000..d402c97a7c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivePublicKeysUseCase.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.card + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.operations.derivation.DerivationTaskResponse +import com.tangem.tap.domain.TangemSdkManager + +// TODO: [REDACTED_JIRA] +internal class DefaultDerivePublicKeysUseCase( + private val tangemSdkManager: TangemSdkManager, +) : DerivePublicKeysUseCase { + + override suspend fun invoke( + cardId: String?, + derivations: Map>, + ): Either { + tangemSdkManager.derivePublicKeys(cardId = cardId, derivations = derivations) + .doOnSuccess { return it.right() } + .doOnFailure { return Unit.left() } + + return Unit.left() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 183ba76207..f1345d6989 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -8,6 +8,8 @@ import java.util.concurrent.CopyOnWriteArrayList /** [REDACTED_AUTHOR] */ +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WarningMessagesManager { private val warningsList = CopyOnWriteArrayList() @@ -73,7 +75,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_developer_card, + // messageResId = R.string.alert_developer_card, origin = WarningMessage.Origin.Local, ) @@ -85,7 +87,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_card_signed_transactions, + // messageResId = R.string.alert_card_signed_transactions, origin = WarningMessage.Origin.Local, ) @@ -96,8 +98,8 @@ class WarningMessagesManager { priority = WarningMessage.Priority.Info, location = listOf(WarningMessage.Location.MainScreen), blockchains = null, - titleResId = R.string.warning_important_security_info, - messageResId = R.string.warning_signed_tx_previously, + // titleResId = R.string.warning_important_security_info, + // messageResId = R.string.warning_signed_tx_previously, origin = WarningMessage.Origin.Local, buttonTextId = R.string.warning_button_learn_more, titleFormatArg = "\u26A0", @@ -147,7 +149,7 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.alert_demo_message, + // messageResId = R.string.alert_demo_message, origin = WarningMessage.Origin.Local, ) @@ -160,14 +162,14 @@ class WarningMessagesManager { location = listOf(WarningMessage.Location.MainScreen), blockchains = null, titleResId = R.string.common_warning, - messageResId = R.string.warning_low_signatures_format, + // messageResId = R.string.warning_low_signatures_format, origin = WarningMessage.Origin.Local, messageFormatArg = remainingSignatures.toString(), ) } - fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { - return warning.messageResId == R.string.alert_card_signed_transactions - } + // fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean { + // return warning.messageResId == R.string.alert_card_signed_transactions + // } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index ec38e494bc..a1e3f77974 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -15,7 +15,10 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.* import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.addContext +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -23,6 +26,7 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -34,8 +38,7 @@ internal object LegacyScanProcessor { allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { return tangemSdkManager.scanProduct( - userTokensRepository, - cardId, + cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, ) } @@ -56,10 +59,7 @@ internal object LegacyScanProcessor { tangemSdkManager.changeDisplayedCardIdNumbersCount(null) - val result = tangemSdkManager.scanProduct( - userTokensRepository = userTokensRepository, - cardId = cardId, - ) + val result = tangemSdkManager.scanProduct(cardId) store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 002b9bc7ff..562ac25718 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -8,13 +8,13 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay internal object UseCaseScanProcessor { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 4f5729135c..cb6471a4ca 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -11,7 +11,6 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.core.chain.Chain import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.setContext @@ -21,6 +20,7 @@ import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import org.rekotlin.Store diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index 2de676ba6c..f74d706775 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -8,13 +8,8 @@ import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter -import com.tangem.tap.domain.tokens.UserTokensRepository internal class DefaultScanCardRepository( - // FIXME: The repository should not depend on another repository. - // But now we need to provide loadBlockchainsToDerive() to ScanProductTask and it's hard to move this method from - // UserTokensRepository. - private val userTokensRepository: UserTokensRepository, private val tangemSdkManager: TangemSdkManager, ) : ScanCardRepository { @@ -27,7 +22,6 @@ internal class DefaultScanCardRepository( when ( val result = tangemSdkManager.scanProduct( cardId = cardId, - userTokensRepository = userTokensRepository, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, ) ) { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 3c0467b1da..b6536cfd4b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -303,7 +303,7 @@ private class CreateWalletTangemWallet( private fun getBlockchains(cardId: String, card: CardDTO): List { return when { - DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains + DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains.toList() card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet) else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt new file mode 100644 index 0000000000..38a23139b6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -0,0 +1,159 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.tap.domain.tokens.UserTokensStorageService +import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal data class BlockchainToDerive( + val blockchain: Blockchain, + val derivationPath: DerivationPath?, +) + +// FIXME: May be move to DI, currently unnecessary +internal class DerivationsFinder( + private val legacyTokensStore: UserTokensStorageService, + private val newTokensStore: UserTokensStore, + private val walletFeatureToggles: WalletFeatureToggles, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun findBlockchainsToDerive( + card: CardDTO, + derivationStyleProvider: DerivationStyleProvider, + ): Set { + if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() + val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() + val derivationStyle = derivationStyleProvider.getDerivationStyle() + + var blockchains = withContext(dispatchers.io) { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + getBlockchainsNew(userWalletId) + } else { + getBlockchainsLegacy(userWalletId) + } + } + + if (blockchains.isEmpty()) { + blockchains = if (DemoHelper.isDemoCardId(card.cardId)) { + getDemoBlockchains(derivationStyle) + } else { + getDefaultBlockchains(derivationStyle) + } + } + + // we should generate second key for cardano + // because cardano address generation for wallet2 requires keys from 2 derivations + // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ + blockchains.addSecondCardanoDerivationIfPresent() + + if (card.settings.isHDWalletAllowed) { + blockchains.addEthereumBlockchains(derivationStyle) + } + + // pay attention to this + if (!card.useOldStyleDerivation) { + blockchains.removeUnnecessaryBlockchains() + } + + return blockchains + } + + private suspend fun getBlockchainsNew(userWalletId: UserWalletId): MutableSet { + val responseTokens = newTokensStore.getSyncOrNull(userWalletId) + ?.tokens + ?: return hashSetOf() + + return responseTokens.asSequence() + .filter { it.contractAddress == null } + .mapNotNull { coin -> + val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null + val derivationPath = coin.derivationPath?.let(::DerivationPath) + + BlockchainToDerive(blockchain, derivationPath) + } + .toMutableSet() + } + + private fun getBlockchainsLegacy(userWalletId: UserWalletId): MutableSet { + val currencies = legacyTokensStore.getUserTokens(userWalletId.stringValue) + ?.takeIf { it.isNotEmpty() } + ?: return hashSetOf() + + return currencies.asSequence() + .filterIsInstance() + .map { coin -> + val blockchain = coin.blockchain + val derivationPath = coin.derivationPath?.let(::DerivationPath) + + BlockchainToDerive(blockchain, derivationPath) + } + .toMutableSet() + } + + // TODO: Move to user wallet config + private fun getDemoBlockchains(derivationStyle: DerivationStyle?): MutableSet { + return DemoHelper.config.demoBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } + + // TODO: Move to user wallet config + private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet { + val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + + return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } +} + +private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { + val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet) + .mapToBlockchainsWithDerivations(derivationStyle) + + addAll(ethereumBlockchains) +} + +private fun MutableSet.removeUnnecessaryBlockchains() { + val unnecessaryBlockchains = listOf( + Blockchain.BSC, Blockchain.BSCTestnet, + Blockchain.Polygon, Blockchain.PolygonTestnet, + Blockchain.RSK, + Blockchain.Fantom, Blockchain.FantomTestnet, + Blockchain.Avalanche, Blockchain.AvalancheTestnet, + ) + + removeAll { it.blockchain in unnecessaryBlockchains } +} + +private fun MutableSet.addSecondCardanoDerivationIfPresent() { + val cardanoDerivation = this + .firstOrNull { it.blockchain == Blockchain.Cardano } + ?.derivationPath + ?: return + + val secondCardanoBlockchain = BlockchainToDerive( + blockchain = Blockchain.Cardano, + derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation), + ) + + add(secondCardanoBlockchain) +} + +private fun Set.mapToBlockchainsWithDerivations( + derivationStyle: DerivationStyle?, +): MutableSet { + return mapTo(hashSetOf()) { blockchain -> + BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index ba72ddb543..a3fac64bc1 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -1,7 +1,5 @@ package com.tangem.tap.domain.tasks.product -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion @@ -15,13 +13,11 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isExcluded import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins -import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TwinsHelper import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider @@ -35,16 +31,14 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError -import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import kotlinx.coroutines.launch import kotlin.collections.set -class ScanProductTask( - val card: Card? = null, - private val userTokensRepository: UserTokensRepository?, - private val additionalBlockchainsToDerive: Collection? = null, +internal class ScanProductTask( + private val card: Card?, + private val derivationsFinder: DerivationsFinder?, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -63,14 +57,14 @@ class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive) + else -> ScanWalletProcessor(derivationsFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult -> when (scanTaskResult) { is CompletionResult.Success -> { - // it need because processorResult.data.card doesn't contains attestation result + // it needed because processorResult.data.card doesn't contains attestation result // and CardWallet.derivedKeys val processorScanResponseWithNewCard = processorResult.data.copy( card = CardDTO(scanTaskResult.data), @@ -99,8 +93,7 @@ class ScanProductTask( } private class ScanWalletProcessor( - private val userTokensRepository: UserTokensRepository?, - private val additionalBlockchainsToDerive: Collection? = null, + private val derivationsFinder: DerivationsFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -244,131 +237,28 @@ private class ScanWalletProcessor( } } - private suspend fun getBlockchainsToDerive( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): List { - val userTokensRepository = userTokensRepository ?: return emptyList() - val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive( - card, - derivationStyleProvider.getDerivationStyle(), - ) - .toMutableList() - .ifEmpty { getDefaultBlockchains(derivationStyleProvider) } - - if (card.settings.isHDWalletAllowed) { - blockchainsToDerive += getEthereumBlockchains(derivationStyleProvider) - } - - additionalBlockchainsToDerive?.let { - blockchainsToDerive += getAdditionalBlockchainToDerive(derivationStyleProvider, it) - } - - // we should generate second key for cardano - // because cardano address generation for wallet2 requires keys from 2 derivations - // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ - val secondCardanoNetwork = blockchainsToDerive - .find { it.blockchain == Blockchain.Cardano } - ?.let { getCardanoSecondNetwork(it) } - secondCardanoNetwork?.let { blockchainsToDerive.add(it) } - - // pay attention to this - if (!card.useOldStyleDerivation) { - removeUnnecessaryBlockchains(blockchainsToDerive, derivationStyleProvider) - } - - return blockchainsToDerive.distinct() - } - private fun getWalletProductType(card: CardDTO): ProductType { if (card.batchId == CardDTO.RING_BATCH_ID) { return ProductType.Ring } - return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available) { + return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available && + card.settings.isKeysImportAllowed + ) { ProductType.Wallet2 } else { ProductType.Wallet } } - private fun getDefaultBlockchains( - derivationStyleProvider: DerivationStyleProvider, - ): MutableList { - return mutableListOf( - BlockchainNetwork( - blockchain = Blockchain.Bitcoin, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - ) - } - - private fun getEthereumBlockchains(derivationStyleProvider: DerivationStyleProvider): List { - return listOf( - BlockchainNetwork( - blockchain = Blockchain.Ethereum, - derivationStyleProvider = derivationStyleProvider, - ), - BlockchainNetwork( - blockchain = Blockchain.EthereumTestnet, - derivationStyleProvider = derivationStyleProvider, - ), - ) - } - - private fun getAdditionalBlockchainToDerive( - derivationStyleProvider: DerivationStyleProvider, - collection: Collection, - ): List { - return collection.map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - } - } - - private fun getCardanoSecondNetwork(cardanoBlockchainNetwork: BlockchainNetwork): BlockchainNetwork? { - val cardanoStandardDerivation = cardanoBlockchainNetwork.derivationPath?.let { DerivationPath(it) } - ?: return null - val cardanoPatchedDerivation = CardanoUtils.extendedDerivationPath(cardanoStandardDerivation) - return BlockchainNetwork( - blockchain = Blockchain.Cardano, - derivationPath = cardanoPatchedDerivation.rawPath, - tokens = emptyList(), - ) - } - - private fun removeUnnecessaryBlockchains( - blockchainsToDerive: MutableList, - derivationStyleProvider: DerivationStyleProvider, - ) { - blockchainsToDerive.removeAll( - listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { - BlockchainNetwork( - blockchain = it, - derivationStyleProvider = derivationStyleProvider, - ) - }, - ) - } - private suspend fun collectDerivations( card: CardDTO, config: CardConfig, derivationStyleProvider: DerivationStyleProvider, ): Map> { - val blockchains = getBlockchainsToDerive(card, derivationStyleProvider) val derivations = mutableMapOf>() + val blockchains = derivationsFinder + ?.findBlockchainsToDerive(card, derivationStyleProvider) + ?: return derivations blockchains.forEach { blockchain -> val curve = config.primaryCurve(blockchain.blockchain) @@ -376,7 +266,7 @@ private class ScanWalletProcessor( if (wallet.chainCode == null) return@forEach val key = wallet.publicKey.toMapKey() - val path = blockchain.derivationPath?.let { DerivationPath(it) } + val path = blockchain.derivationPath if (path != null) { val addedDerivations = derivations[key] if (addedDerivations != null) { @@ -386,6 +276,7 @@ private class ScanWalletProcessor( } } } + return derivations } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt deleted file mode 100644 index 39e845f7b2..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.squareup.moshi.JsonAdapter -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.services.Result -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class LoadAvailableCoinsService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val assetReader: AssetReader, -) { - private val currenciesAdapter: JsonAdapter = - MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java) - - suspend fun getSupportedTokens( - isTestNet: Boolean, - supportedBlockchains: List, - page: Int, - searchInput: String?, - ): Result { - if (isTestNet) { - return Result.Success( - LoadedCoins( - currencies = getTestnetCoins().filter(searchInput), - moreAvailable = false, - ), - ) - } - - val offset = page * LOAD_PER_PAGE - return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) { - is Result.Success -> { - val data = result.data - - Result.Success( - LoadedCoins( - currencies = data.coins.map { - Currency.fromCoinResponse(currency = it, imageHost = data.imageHost) - }, - moreAvailable = data.total > offset + LOAD_PER_PAGE, - ), - ) - } - is Result.Failure -> { - Result.Failure(result.error) - } - } - } - - private suspend fun loadCoins( - supportedBlockchains: List, - offset: Int, - searchInput: String?, - ): Result { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - networkIds = supportedBlockchains.joinToString( - separator = ",", - transform = Blockchain::toNetworkId, - ), - active = true, - searchText = searchInput, - offset = offset, - limit = LOAD_PER_PAGE, - ) - }.fold( - onSuccess = { Result.Success(it) }, - onFailure = { Result.Failure(it) }, - ) - } - } - - private fun getTestnetCoins(): List { - val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) - return currenciesAdapter.fromJson(json)!!.coins - .map { Currency.fromJsonObject(it) } - } - - private fun List.filter(searchInput: String?): List { - if (searchInput.isNullOrBlank()) return this - - return filter { currency -> - currency.symbol.contains(searchInput, ignoreCase = true) || - currency.name.contains(searchInput, ignoreCase = true) - } - } - - private companion object { - const val LOAD_PER_PAGE = 100 - const val FILE_NAME_TESTNET_COINS = "testnet_tokens" - } -} - -data class LoadedCoins( - val currencies: List, - val moreAvailable: Boolean, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 7026203db3..c3cfe566f3 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -1,13 +1,12 @@ package com.tangem.tap.domain.tokens -import android.content.Context import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.core.TangemSdkError +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.files.AndroidFileReader import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.userwallets.UserWalletIdBuilder @@ -108,7 +107,8 @@ class UserTokensRepository( return runCatching { tangemTechApi.getUserTokens(userWalletId) } .fold( onSuccess = { response -> - response.tokens + response.getOrThrow() + .tokens .mapNotNull(Currency.Companion::fromTokenResponse) .also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) } .distinct() @@ -131,12 +131,12 @@ class UserTokensRepository( // TODO("After adding DI") get dependencies by DI fun init( - context: Context, tangemTechService: TangemTechService, networkConnectionManager: NetworkConnectionManager, + storageService: UserTokensStorageService, ): UserTokensRepository { return UserTokensRepository( - storageService = UserTokensStorageService(fileReader = AndroidFileReader(context)), + storageService = storageService, tangemTechApi = tangemTechService.api, dispatchers = AppCoroutineDispatcherProvider(), networkConnectionManager = networkConnectionManager, diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index ff90f83942..08fb2c4ce4 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -1,9 +1,11 @@ package com.tangem.tap.domain.tokens +import android.content.Context import com.squareup.moshi.JsonAdapter import com.tangem.Log import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.files.AndroidFileReader import com.tangem.datasource.files.FileReader import com.tangem.tap.features.wallet.models.Currency @@ -30,5 +32,7 @@ class UserTokensStorageService(private val fileReader: FileReader) { companion object { private const val FILE_NAME_PREFIX_USER_TOKENS = "user_tokens" private fun getFileNameForUserTokens(userId: String): String = "${FILE_NAME_PREFIX_USER_TOKENS}_$userId" + + fun init(context: Context) = UserTokensStorageService(AndroidFileReader(context)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 1ece7060b3..4e3364feba 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -23,7 +23,7 @@ class FinalizeTwinTask( PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> - ScanProductTask(readResult.data, null) + ScanProductTask(readResult.data, derivationsFinder = null) .run(session, callback) is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt index 5f02cd1258..719491a036 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt @@ -3,25 +3,28 @@ package com.tangem.tap.domain.userWalletList.di import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.Provider +import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager +import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager +import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.json.* +import com.tangem.tap.tangemSdkManager private const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage" fun UserWalletsListManager.Companion.provideBiometricImplementation( - context: Context, - tangemSdkManager: TangemSdkManager, + applicationContext: Context, ): UserWalletsListManager { val moshi = Moshi.Builder() .add(WalletDerivedKeysMapAdapter()) @@ -38,15 +41,25 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation( val secureStorage = AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( - context = context, + context = applicationContext, storageName = USER_WALLETS_STORAGE_NAME, ), ) + val authenticatedStorage = AuthenticatedStorage( + secureStorage = UserWalletsKeysStoreDecorator( + featureStorage = secureStorage, + cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, + ), + keystoreManager = DelegatedKeystoreManager( + keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, + ), + ) + val keysRepository = BiometricUserWalletsKeysRepository( moshi = moshi, secureStorage = secureStorage, - biometricManager = tangemSdkManager.biometricManager, + authenticatedStorage = authenticatedStorage, ) val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( moshi = moshi, 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 76a94f34bd..f4c32feb54 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 @@ -57,21 +57,11 @@ internal class BiometricUserWalletsListManager( get() = state.value.userWallets.size override suspend fun unlock(): CompletionResult { - return unlockWithBiometryInternal() - .mapFailure { error -> - if (error is UserWalletsListError) { - error - } else { - UserWalletsListError.UnableToUnlockUserWallets(cause = error) - } - } - .map { - selectedUserWalletSync.guard { - throw UserWalletsListError.UnableToUnlockUserWallets( - cause = IllegalStateException("No user wallet selected"), - ) - } - } + return unlockWithBiometryInternal().mapUnlockResult() + } + + override suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult { + return unlockWithBiometryInternal(selectedWalletId = selectedWalletId).mapUnlockResult() } override fun lock() { @@ -141,7 +131,7 @@ internal class BiometricUserWalletsListManager( return sensitiveInformationRepository.delete(idsToRemove) .flatMap { publicInformationRepository.delete(idsToRemove) } - .flatMap { keysRepository.delete(idsToRemove) } + .map { keysRepository.delete(idsToRemove) } .map { state.update { prevState -> val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove } @@ -158,7 +148,7 @@ internal class BiometricUserWalletsListManager( override suspend fun clear(): CompletionResult { return sensitiveInformationRepository.clear() .flatMap { publicInformationRepository.clear() } - .flatMap { keysRepository.clear() } + .map { keysRepository.clear() } .map { selectedUserWalletRepository.set(null) lock() @@ -198,7 +188,7 @@ internal class BiometricUserWalletsListManager( } } - private suspend fun unlockWithBiometryInternal(): CompletionResult { + private suspend fun unlockWithBiometryInternal(selectedWalletId: UserWalletId? = null): CompletionResult { return keysRepository.getAll() .map { keys -> state.update { prevState -> @@ -207,7 +197,7 @@ internal class BiometricUserWalletsListManager( ) } } - .flatMap { loadModels() } + .flatMap { loadModels(selectedWalletId = selectedWalletId) } .map { state.update { prevState -> val hasLockedUserWallets = prevState.userWallets.any { it.isLocked } @@ -216,6 +206,24 @@ internal class BiometricUserWalletsListManager( } } + private fun CompletionResult.mapUnlockResult(): CompletionResult { + return this + .mapFailure { error -> + if (error is UserWalletsListError) { + error + } else { + UserWalletsListError.UnableToUnlockUserWallets(cause = error) + } + } + .map { + selectedUserWalletSync.guard { + throw UserWalletsListError.UnableToUnlockUserWallets( + cause = IllegalStateException("No user wallet selected"), + ) + } + } + } + private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult { val encryptionKey = userWallet.scanResponse.card.encryptionKey ?.let { UserWalletEncryptionKey(userWallet.walletId, it) } @@ -237,7 +245,7 @@ internal class BiometricUserWalletsListManager( } } - private suspend fun loadModels(): CompletionResult { + private suspend fun loadModels(selectedWalletId: UserWalletId? = null): CompletionResult { return getSavedUserWallets() .map { userWallets -> if (userWallets.isNotEmpty()) { @@ -247,7 +255,7 @@ internal class BiometricUserWalletsListManager( prevState.copy( userWallets = wallets, selectedUserWalletId = findOrSetSelectedUserWalletId( - prevSelectedWalletId = prevState.selectedUserWalletId, + prevSelectedWalletId = selectedWalletId ?: prevState.selectedUserWalletId, userWallets = wallets, ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt new file mode 100644 index 0000000000..64e0f9a1ce --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DelegatedKeystoreManager.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.tangem.common.Provider +import com.tangem.common.authentication.KeystoreManager +import javax.crypto.SecretKey + +internal class DelegatedKeystoreManager( + private val keystoreManagerProvider: Provider, +) : KeystoreManager { + + override suspend fun authenticateAndGetKey(keyAlias: String): SecretKey? { + return keystoreManagerProvider().authenticateAndGetKey(keyAlias) + } + + override suspend fun storeKey(keyAlias: String, key: SecretKey) { + keystoreManagerProvider().storeKey(keyAlias, key) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt index 08c509a16c..170296fbe1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt @@ -23,15 +23,13 @@ internal interface UserWalletsKeysRepository { /** * Delete encryption keys for user wallets. Biometric authentication not required * @param userWalletsIds List of [UserWalletId] whose encryption keys will be deleted - * @return [CompletionResult] of operation * */ - suspend fun delete(userWalletsIds: List): CompletionResult + suspend fun delete(userWalletsIds: List) /** * Clear all encryption keys for user wallets. Biometric authentication not required - * @return [CompletionResult] of operation * */ - suspend fun clear(): CompletionResult + suspend fun clear() /** * Determine if the user has saved user wallets diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt new file mode 100644 index 0000000000..49e87a395a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysStoreDecorator.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.domain.userWalletList.repository + +import com.tangem.common.Provider +import com.tangem.common.services.secure.SecureStorage + +/** + * A decorator for [SecureStorage] that facilitates data migration between two storages. + * + * @property featureStorage The primary storage, which will eventually contain all user data. + * @property cardSdkStorageProvider The SDK's storage where user data might have been previously stored. + */ +internal class UserWalletsKeysStoreDecorator( + private val featureStorage: SecureStorage, + private val cardSdkStorageProvider: Provider, +) : SecureStorage by featureStorage { + + override fun delete(account: String) { + featureStorage.delete(account) + cardSdkStorageProvider().delete(account) + } + + override fun get(account: String): ByteArray? { + var data = featureStorage.get(account) + + if (data == null) { + data = cardSdkStorageProvider().get(account) ?: return null + featureStorage.store(data, account) + } + + return data + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt index 0d4dd5d1c5..bb40308916 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt @@ -4,8 +4,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.tangem.common.* -import com.tangem.common.biometric.BiometricManager -import com.tangem.common.biometric.BiometricStorage +import com.tangem.common.authentication.AuthenticatedStorage import com.tangem.common.core.TangemSdkError import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.legacy.UserWalletsListError @@ -18,13 +17,10 @@ import kotlinx.coroutines.withContext internal class BiometricUserWalletsKeysRepository( moshi: Moshi, - biometricManager: BiometricManager, + private val authenticatedStorage: AuthenticatedStorage, private val secureStorage: SecureStorage, ) : UserWalletsKeysRepository { - private val biometricStorage = BiometricStorage( - biometricManager = biometricManager, - secureStorage = secureStorage, - ) + private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( UserWalletEncryptionKey::class.java, ) @@ -37,13 +33,13 @@ internal class BiometricUserWalletsKeysRepository( getAllInternal() .mapFailure { error -> when (error) { - is TangemSdkError.BiometricsAuthenticationLockout -> + is TangemSdkError.AuthenticationLockout -> UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false) - is TangemSdkError.BiometricsAuthenticationPermanentLockout -> + is TangemSdkError.AuthenticationPermanentLockout -> UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true) - is TangemSdkError.BiometricCryptographyKeyInvalidated -> + is TangemSdkError.KeystoreInvalidated -> UserWalletsListError.EncryptionKeyInvalidated - is TangemSdkError.BiometricsUnavailable -> + is TangemSdkError.AuthenticationUnavailable -> UserWalletsListError.BiometricsAuthenticationDisabled else -> error } @@ -57,26 +53,24 @@ internal class BiometricUserWalletsKeysRepository( } } - override suspend fun delete(userWalletsIds: List): CompletionResult { + override suspend fun delete(userWalletsIds: List) { return withContext(Dispatchers.IO) { - userWalletsIds.map { userWalletId -> + userWalletsIds.forEach { userWalletId -> deleteEncryptionKey(userWalletId) } - .fold() - .map { deleteUserWalletsIds(userWalletsIds) } + + deleteUserWalletsIds(userWalletsIds) } } - override suspend fun clear(): CompletionResult { + override suspend fun clear() { return withContext(Dispatchers.IO) { getUserWalletsIds() - .map { userWalletId -> + .forEach { userWalletId -> deleteEncryptionKey(userWalletId) } - .fold() - .map { - clearUserWalletsIds() - } + + clearUserWalletsIds() } } @@ -89,35 +83,20 @@ internal class BiometricUserWalletsKeysRepository( private suspend fun getAllInternal(): CompletionResult> { return getUserWalletsIds() .map { userWalletId -> - // It is possible to request multiple user wallet keys from biometric storage because - // the biometric cryptography key has an expiration time. - // If this operation runs more than that expiration time, then the user will have to re-authorize - // to receive all user wallets encryption keys getEncryptionKey(userWalletId) - .flatMapOnFailure { error -> - when (error) { - is TangemSdkError.InvalidBiometricCryptographyKey, - is TangemSdkError.BiometricCryptographyOperationFailed, - -> { - // These errors can be skipped as the user has the option to re-save their wallets - // in case they occur - CompletionResult.Success(data = null) - } - else -> CompletionResult.Failure(error) - } - } .doOnFailure { error -> when (error) { - is TangemSdkError.UserCanceledBiometricsAuthentication -> { + is TangemSdkError.UserCanceledAuthentication -> { // If the user cancels biometric authentication, then cancel operation with error return CompletionResult.Failure(error) } - is TangemSdkError.BiometricCryptographyKeyInvalidated -> { + is TangemSdkError.KeystoreInvalidated -> { // If the biometric cryptography key was invalidated, // then delete all user wallets encryption keys and cancel operation with error getUserWalletsIds().forEach { userWalletId -> deleteEncryptionKey(userWalletId) } + return CompletionResult.Failure(error) } } @@ -129,20 +108,22 @@ internal class BiometricUserWalletsKeysRepository( } private suspend fun getEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) + return catching { authenticatedStorage.get(StorageKey.UserWalletEncryptionKey(userWalletId).name) } .map { it.decodeToKey() } } private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { - return biometricStorage.store( - key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, - data = encryptionKey.encode(), - ) + return catching { + authenticatedStorage.store( + key = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } .map { storeUserWalletId(encryptionKey.walletId) } } - private suspend fun deleteEncryptionKey(userWalletId: UserWalletId): CompletionResult { - return biometricStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + private fun deleteEncryptionKey(userWalletId: UserWalletId) { + return authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) } private suspend fun getUserWalletsIds(): List { diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 006bca6820..883b3a821d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -1,18 +1,17 @@ package com.tangem.tap.domain.walletStores.repository.implementation import com.tangem.blockchain.common.* -import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.CompletionResult import com.tangem.common.catching import com.tangem.common.doOnSuccess import com.tangem.common.mapFailure import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.makeWalletManagerForApp import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.models.UserWallet @@ -70,7 +69,7 @@ internal class DefaultWalletManagersRepository( } val derivationParams = getDerivationParams( derivationPath = blockchainNetwork?.derivationPath, - card = scanResponse.card, + derivationStyleProvider = scanResponse.derivationStyleProvider, ) val walletManager = blockchain?.let { @@ -209,17 +208,16 @@ internal class DefaultWalletManagersRepository( } } - private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? { - return derivationPath?.let { - DerivationParams.Custom( - path = DerivationPath(it), - ) - } ?: if (!card.settings.isHDWalletAllowed) { - null - } else if (card.useOldStyleDerivation) { - DerivationParams.Default(DerivationStyle.LEGACY) + private fun getDerivationParams( + derivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): DerivationParams? { + val derivationStyle = derivationStyleProvider.getDerivationStyle() ?: return null + + return if (derivationPath == null) { + DerivationParams.Default(derivationStyle) } else { - DerivationParams.Default(DerivationStyle.NEW) + DerivationParams.Custom(DerivationPath(derivationPath)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index cd6161868b..e7aa8ff258 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -30,12 +30,6 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP configureTransitions() } - protected open fun configureTransitions() { - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { mainView = super.onCreateView(inflater, container, savedInstanceState)!! return mainView @@ -56,6 +50,16 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP protected open fun loadToolbarMenu(): MenuProvider? = null + protected open fun configureTransitions() { + configureDefaultTransactions() + } + + protected fun configureDefaultTransactions() { + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.fade) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + fun showRetrySnackbar(message: String, action: VoidCallback) { val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE) snackbar.setAction(getString(R.string.common_retry)) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt index b64f9efd14..a6894e04b1 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt @@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles */ interface CustomTokenFeatureToggles { - /** Availability of redesigned screen (internal feature) */ - val isRedesignedScreenEnabled: Boolean - val isNewCardScanningEnabled: Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt index 4ae3926d84..193a65643f 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.customtoken.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor @@ -24,6 +25,7 @@ internal object CustomTokenInteractorModule { fun provideCustomTokenInteractor( tangemTechApi: TangemTechApi, appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, reduxStateHolder: AppStateHolder, ): CustomTokenInteractor { return DefaultCustomTokenInteractor( @@ -32,7 +34,7 @@ internal object CustomTokenInteractorModule { dispatchers = appCoroutineDispatcherProvider, reduxStateHolder = reduxStateHolder, ), - reduxStateHolder = reduxStateHolder, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, ) } } \ 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 f104a8ca09..473a81eeef 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 @@ -9,12 +9,17 @@ import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -24,21 +29,22 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import timber.log.Timber /** * Default implementation of custom token interactor * * @property featureRepository feature repository - * @property reduxStateHolder redux state holder * [REDACTED_AUTHOR] */ class DefaultCustomTokenInteractor( private val featureRepository: CustomTokenRepository, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, ) : CustomTokenInteractor { override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken { @@ -49,28 +55,30 @@ class DefaultCustomTokenInteractor( } override suspend fun saveToken(customCurrency: CustomCurrency) { - val scanResponse = reduxStateHolder.scanResponse ?: return + val userWallet = getSelectedWalletSyncUseCase().fold(ifLeft = { return }, ifRight = { it }) val currency = Currency.fromCustomCurrency(customCurrency) - val isNeedToDerive = isNeedToDerive(scanResponse, currency) + val isNeedToDerive = isNeedToDerive(userWallet, currency) if (isNeedToDerive) { - deriveMissingBlockchains(scanResponse = scanResponse, currencyList = listOf(currency)) { - submitAdd(scanResponse = it, currency = currency) + deriveMissingBlockchains(userWallet = userWallet, currencyList = listOf(currency)) { + submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) } } else { - submitAdd(scanResponse, currency) + submitAdd(userWallet, currency) } } - private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { + private fun isNeedToDerive(userWallet: UserWallet, currency: Currency): Boolean { + val scanResponse = userWallet.scanResponse return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false } private suspend fun deriveMissingBlockchains( - scanResponse: ScanResponse, + userWallet: UserWallet, currencyList: List, onSuccess: suspend (ScanResponse) -> Unit, ) { + val scanResponse = userWallet.scanResponse val config = CardConfig.createConfig(scanResponse.card) val derivationDataList = currencyList.mapNotNull { currency -> val curve = config.primaryCurve(currency.blockchain) @@ -153,7 +161,42 @@ class DefaultCustomTokenInteractor( return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } - private suspend fun submitAdd(scanResponse: ScanResponse, currency: Currency) { + private suspend fun submitAdd(userWallet: UserWallet, currency: Currency) { + val scanResponse = userWallet.scanResponse + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + + if (walletFeatureToggles.isRedesignedScreenEnabled) { + val cryptoCurrencyFactory = CryptoCurrencyFactory() + + submitNewAdd( + userWalletId = userWallet.walletId, + updatedScanResponse = scanResponse, + currencyList = listOfNotNull( + when (currency) { + is Currency.Blockchain -> { + cryptoCurrencyFactory.createCoin( + blockchain = currency.blockchain, + extraDerivationPath = currency.derivationPath, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } + is Currency.Token -> { + cryptoCurrencyFactory.createToken( + sdkToken = currency.token, + blockchain = currency.blockchain, + extraDerivationPath = currency.derivationPath, + derivationStyleProvider = scanResponse.derivationStyleProvider, + ) + } + }, + ), + ) + } else { + submitLegacyAdd(scanResponse = scanResponse, currency = currency) + } + } + + private suspend fun submitLegacyAdd(scanResponse: ScanResponse, currency: Currency) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -170,4 +213,27 @@ class DefaultCustomTokenInteractor( ) } } + + private fun submitNewAdd( + userWalletId: UserWalletId, + updatedScanResponse: ScanResponse, + currencyList: List, + ) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository) + scope.launch { + userWalletsListManager.update( + userWalletId = userWalletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + val networks = currencyList.map { it.network }.toSet() + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = networks, + refresh = true, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt index a19dde5403..cd06665b00 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt @@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : CustomTokenFeatureToggles { - override val isRedesignedScreenEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED") - override val isNewCardScanningEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED") } \ No newline at end of file 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 a203e348dd..3900824553 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 @@ -1,21 +1,18 @@ package com.tangem.tap.features.customtoken.impl.presentation -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel -import androidx.transition.TransitionInflater +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel -import com.tangem.wallet.R import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** * Add custom token screen @@ -23,29 +20,23 @@ import dagger.hilt.android.AndroidEntryPoint [REDACTED_AUTHOR] */ @AndroidEntryPoint -internal class AddCustomTokenFragment : Fragment() { +internal class AddCustomTokenFragment : ComposeFragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - with(TransitionInflater.from(requireContext())) { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Composable + override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel().apply { + LocalLifecycleOwner.current.lifecycle.addObserver(this) } - - return ComposeView(inflater.context).apply { - setContent { - isTransitionGroup = true - - val viewModel = hiltViewModel().apply { - LocalLifecycleOwner.current.lifecycle.addObserver(this) - } - - TangemTheme { - AddCustomTokenScreen( - modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, - ) - } - } + val statusBarColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(color = statusBarColor) } + AddCustomTokenScreen( + modifier = Modifier.systemBarsPadding(), + stateHolder = viewModel.uiState, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt index d69f1c08fa..ba881741b0 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenPreviewData.kt @@ -55,7 +55,7 @@ internal object AddCustomTokenPreviewData { value = "", onValueChange = {}, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_token_symbol_input_title), + label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old), placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder), isEnabled = false, ), diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt index eb099e7ee8..3824462da3 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt @@ -25,10 +25,20 @@ internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifi @Preview(showSystemUi = true) @Composable -private fun Preview_AddCustomTokenScreen( +private fun Preview_AddCustomTokenScreen_Light( @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, ) { - TangemTheme { + TangemTheme(isDark = false) { + AddCustomTokenScreen(stateHolder) + } +} + +@Preview(showSystemUi = true) +@Composable +private fun Preview_AddCustomTokenScreen_Dark( + @PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder, +) { + TangemTheme(isDark = true) { AddCustomTokenScreen(stateHolder) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt index 9304675ad7..48ffb09110 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt @@ -91,7 +91,7 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont floatingActionButtonPosition = FabPosition.Center, sheetBackgroundColor = TangemTheme.colors.background.secondary, sheetPeekHeight = TangemTheme.dimens.size0, - backgroundColor = TangemTheme.colors.background.secondary, + backgroundColor = TangemTheme.colors.background.primary, ) { Column( modifier = Modifier diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt index 0090ef8223..fb6c260728 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt @@ -94,7 +94,7 @@ private fun TextField(model: AddCustomTokenInputField, isError: Boolean) { label = { Text( text = model.label.resolveReference(), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor( enabled = isEnabled, error = isError, @@ -178,7 +178,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) { text = subtitle, color = TangemTheme.colors.text.secondary, maxLines = 1, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt index 8587b990bf..81d0f52912 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt @@ -1,11 +1,9 @@ package com.tangem.tap.features.customtoken.impl.presentation.ui.components +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -25,7 +23,15 @@ import com.tangem.tap.features.details.ui.cardsettings.resolveReference */ @Composable internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) { - TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) { + val toolbarElevation = if (isSystemInDarkTheme()) { + TangemTheme.dimens.elevation0 + } else { + AppBarDefaults.TopAppBarElevation + } + TopAppBar( + backgroundColor = TangemTheme.colors.background.primary, + elevation = toolbarElevation, + ) { IconButton(onClick = onBackButtonClick) { Icon( painter = painterResource(id = R.drawable.ic_back_24), diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt index 7627e3c6af..a00fc2d4fb 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarnings.kt @@ -72,8 +72,16 @@ private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) { @Preview @Composable -private fun Preview_AddCustomTokenWarnings() { +private fun Preview_AddCustomTokenWarnings_Light() { TangemTheme { AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) } +} + +@Preview +@Composable +private fun Preview_AddCustomTokenWarnings_Dark() { + TangemTheme(isDark = true) { + AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings()) + } } \ No newline at end of file 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 dffe1709dd..5926da1a71 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 @@ -22,6 +22,10 @@ import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.features.addCustomToken.CustomCurrency +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken @@ -35,7 +39,6 @@ import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactA import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching @@ -51,22 +54,24 @@ import javax.inject.Inject /** * ViewModel for add custom token screen * - * @param analyticsEventHandler analytics event handler - * @param featureRouter feature router - * @property featureInteractor feature interactor - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder + * @param analyticsEventHandler analytics event handler + * @param featureRouter feature router + * @property featureInteractor feature interactor + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class AddCustomTokenViewModel @Inject constructor( analyticsEventHandler: AnalyticsEventHandler, featureRouter: CustomTokenRouter, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val featureInteractor: CustomTokenInteractor, private val dispatchers: AppCoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { private val analyticsSender = AddCustomTokenAnalyticsSender(analyticsEventHandler) @@ -74,12 +79,30 @@ internal class AddCustomTokenViewModel @Inject constructor( private val testActionsHandler = TestActionsHandler() private val formStateBuilder = FormStateBuilder() + private var currentCryptoCurrencies: List = emptyList() + /** Screen state */ var uiState by mutableStateOf(getInitialUiState()) private set private var foundToken: FoundToken? = null + init { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + viewModelScope.launch(dispatchers.main) { + currentCryptoCurrencies = getSelectedWalletSyncUseCase().fold( + ifLeft = { emptyList() }, + ifRight = { selectedWallet -> + getCurrenciesUseCase(selectedWallet.walletId).fold( + ifLeft = { emptyList() }, + ifRight = { it }, + ) + }, + ) + } + } + } + override fun onCreate(owner: LifecycleOwner) { analyticsSender.sendWhenScreenOpened() } @@ -208,7 +231,10 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getNetworkSelectorItems(): List { val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown) - val scanResponse = reduxStateHolder.scanResponse + val scanResponse = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { it.scanResponse }, + ) val derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle() return listOf(defaultNetwork) + Blockchain.values() .filter { blockchain -> @@ -244,7 +270,7 @@ internal class AddCustomTokenViewModel @Inject constructor( value = "", onValueChange = actionsHandler::onTokenSymbolValueChange, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - label = TextReference.Res(R.string.custom_token_token_symbol_input_title), + label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old), placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder), isEnabled = false, ) @@ -262,17 +288,20 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? { - val scanResponse = reduxStateHolder.scanResponse - if (scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - val selectorItems = - getDerivationPathsSelectorItems(scanResponse?.derivationStyleProvider) - return AddCustomTokenSelectorField.DerivationPath( - label = TextReference.Res(R.string.custom_token_derivation_path_input_title), - selectedItem = requireNotNull(selectorItems.firstOrNull()), - items = selectorItems, - onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, - isEnabled = true, + val selectorItems = getDerivationPathsSelectorItems(it.scanResponse.derivationStyleProvider) + AddCustomTokenSelectorField.DerivationPath( + label = TextReference.Res(R.string.custom_token_derivation_path_input_title), + selectedItem = requireNotNull(selectorItems.firstOrNull()), + items = selectorItems, + onMenuItemClick = actionsHandler::onDerivationPathSelectorItemClick, + isEnabled = true, + ) + }, ) } @@ -315,14 +344,19 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? { - if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + return getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { + if (!it.scanResponse.card.settings.isHDWalletAllowed) return null - return AddCustomTokenInputField.DerivationPath( - value = "", - onValueChange = actionsHandler::onDerivationPathValueChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - label = TextReference.Res(R.string.custom_token_custom_derivation), - placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + AddCustomTokenInputField.DerivationPath( + value = "", + onValueChange = actionsHandler::onDerivationPathValueChange, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + label = TextReference.Res(R.string.custom_token_custom_derivation), + placeholder = TextReference.Str(value = DERIVATION_PATH_PLACEHOLDER), + ) + }, ) } } @@ -440,11 +474,15 @@ internal class AddCustomTokenViewModel @Inject constructor( val isSupportedToken = if (!isNetworkSelected()) { true } else { - val scanResponse = reduxStateHolder.scanResponse - scanResponse?.card?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false + getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } return buildSet { @@ -478,13 +516,16 @@ internal class AddCustomTokenViewModel @Inject constructor( address = uiState.form.contractAddressInputField.value, blockchain = networkSelectorValue, ) - val scanResponse = reduxStateHolder.scanResponse - val isSupportedToken = scanResponse?.card - ?.canHandleToken( - blockchain = networkSelectorValue, - cardTypesResolver = scanResponse.cardTypesResolver, - ) - ?: false + + val isSupportedToken = getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + it.scanResponse.card.canHandleToken( + blockchain = networkSelectorValue, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) uiState.copySealed( floatingButton = uiState.floatingButton.copy( @@ -546,6 +587,33 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isTokenAlreadyAdded(): Boolean { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + isTokenAlreadyAddedNew() + } else { + isTokenAlreadyAddedOld() + } + } + + private fun isTokenAlreadyAddedNew(): Boolean { + 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 savedTokenId = if (token.isCustom) null else token.id.value + + val sameId = foundToken?.id == savedTokenId + val sameAddress = contractAddress == token.contractAddress + val sameBlockchain = networkId == token.network.id.value + val isSameDerivationPath = getDerivationPath()?.rawPath == token.network.derivationPath.value + + sameId && sameAddress && sameBlockchain && isSameDerivationPath + } + } + + private fun isTokenAlreadyAddedOld(): Boolean { return store.state.walletState.walletsStores .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } .flatten() @@ -563,6 +631,23 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isBlockchainAlreadyAdded(): Boolean { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + isBlockchainAlreadyAddedNew() + } else { + isBlockchainAlreadyAddedOld() + } + } + + private fun isBlockchainAlreadyAddedNew(): Boolean { + return currentCryptoCurrencies + .filterIsInstance() + .any { coin -> + coin.network.id.value == uiState.form.networkSelectorField.selectedItem.blockchain.id && + coin.network.derivationPath.value == getDerivationPath()?.rawPath + } + } + + private fun isBlockchainAlreadyAddedOld(): Boolean { return store.state.walletState.walletsStores .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } .flatten() @@ -641,8 +726,12 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun getDerivationPathForBlockchain(blockchain: Blockchain?): DerivationPath? { if (blockchain == null) return null - val derivationStyle = reduxStateHolder.scanResponse?.derivationStyleProvider?.getDerivationStyle() - ?: DerivationStyle.V1 + val derivationStyle = getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { + it.scanResponse.derivationStyleProvider.getDerivationStyle() + }, + ) val derivationNetwork = if (blockchain == Blockchain.Unknown) { uiState.form.networkSelectorField.selectedItem.blockchain @@ -653,12 +742,15 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean { - val scanResponse = reduxStateHolder.scanResponse - val canHandleToken = scanResponse?.card?.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - return !canHandleToken + return getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + !it.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) { @@ -827,7 +919,6 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onResetButtonClick() { - val scanResponse = reduxStateHolder.scanResponse with(uiState.form) { uiState = uiState.copySealed( form = uiState.form.copy( diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt deleted file mode 100644 index 204f95ec25..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy - -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import androidx.appcompat.widget.Toolbar -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.analytics.events.ManageTokens -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.FragmentOnBackPressedHandler -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen -import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -/** -[REDACTED_AUTHOR] - */ -class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber { - - private var state: MutableState = mutableStateOf(domainStore.state.addCustomTokensState) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Analytics.send(ManageTokens.CustomToken.ScreenOpened) - } - - override fun subscribeToStore() { - domainStore.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.addCustomTokensState == newState.addCustomTokensState - }.select { it.addCustomTokensState } - } - } - - override fun newState(state: AddCustomTokenState) { - if (activity == null || view == null) return - - this.state.value = state - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - view.findViewById(R.id.toolbar)?.setTitle(R.string.add_custom_token_title) - - val closePopupTrigger = initClosingPopupTriggerEvent() - view.findViewById(R.id.view_compose)?.setContent { - TangemTheme { - Box( - modifier = Modifier - .fillMaxSize(), - ) { - AddCustomTokenScreen(state, closePopupTrigger) - } - } - } - } - - private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply { - onCloseComplete = ::handleOnBackPressed - addBackPressHandler( - object : FragmentOnBackPressedHandler { - override fun handleOnBackPressed() = close() - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt deleted file mode 100644 index baa487cec7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.common.form.DataField -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.features.addCustomToken.redux.ScreenState -import com.tangem.domain.features.addCustomToken.redux.ViewStates -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.AddCustomTokenWarning -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AddCustomTokenScreen(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) } - - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), - ) - val coroutineScope = rememberCoroutineScope() - val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } } - - BottomSheetScaffold( - scaffoldState = bottomSheetScaffoldState, - sheetContent = { - Surface(color = colorResource(id = R.color.lightGray5)) { - selectedTestCase.value.content(toggleBottomSheet) - } - }, - sheetPeekHeight = 0.dp, - ) { - Column { - TestCasesList( - onItemClick = { - selectedTestCase.value = it - toggleBottomSheet() - }, - ) - ScreenContent(state, closePopupTrigger) - } - } - - ComposeDialogManager() - LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) - DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) -} - -@OptIn(ExperimentalMaterialApi::class) -private suspend fun BottomSheetScaffoldState.toggle() { - if (bottomSheetState.isCollapsed) { - bottomSheetState.expand() - } else { - bottomSheetState.collapse() - } -} - -@Composable -private fun ScreenContent(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val scaffoldState = rememberScaffoldState() - - Scaffold( - scaffoldState = scaffoldState, - backgroundColor = colorResource(id = R.color.backgroundLightGray), - floatingActionButton = { - HangingOverKeyboardView(keyboardState = keyboardAsState()) { - AddButton(state) - } - }, - floatingActionButtonPosition = FabPosition.Center, - ) { paddings -> - Box( - modifier = Modifier - .padding(paddings) - .fillMaxSize(), - ) { - LazyColumn( - contentPadding = PaddingValues(bottom = 90.dp), - ) { - item { - Surface( - modifier = Modifier.padding(16.dp), - shape = MaterialTheme.shapes.small, - elevation = 4.dp, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - FormFields(state, closePopupTrigger) - } - } - } - item { Warnings(state.value.warnings.toList()) } - } - } - } -} - -@Composable -private fun FormFields(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - - val stateValue = state.value - stateValue.form.fieldList.forEach { field -> - val data = ScreenFieldData.fromState(field, stateValue, errorConverter) - when (field.id) { - ContractAddress -> TokenContractAddressView(data) - Network -> TokenNetworkView(data, stateValue, closePopupTrigger) - Name -> TokenNameView(data) - Symbol -> TokenSymbolView(data) - Decimals -> TokenDecimalsView(data) - DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger) - } - } -} - -@Composable -fun Warnings(warnings: List) { - if (warnings.isEmpty()) return - - val context = LocalContext.current - val warningConverter = remember { ModuleMessageConverter(context) } - - Column { - warnings.forEachIndexed { index, item -> - val modifier = when (index) { - 0 -> Modifier.padding(vertical = 0.dp) - warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp) - else -> Modifier.padding(top = 8.dp, bottom = 0.dp) - } - AddCustomTokenWarning( - modifier = modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - warning = item, - converter = warningConverter, - ) - } - } -} - -@Composable -private fun AddButton(state: MutableState) { - PrimaryButtonIconStart( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.common_add), - iconResId = R.drawable.ic_plus_24, - enabled = state.value.screenState.addButton.isEnabled, - onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) }, - ) -} - -data class ScreenFieldData( - val field: DataField<*>, - val error: AddCustomTokenError?, - val errorConverter: ModuleMessageConverter, - val viewState: ViewStates.TokenField, -) { - companion object { - fun fromState( - field: DataField<*>, - state: AddCustomTokenState, - errorConverter: ModuleMessageConverter, - ): ScreenFieldData { - return ScreenFieldData( - field = field, - error = state.getError(field.id), - errorConverter = errorConverter, - viewState = selectField(field.id, state.screenState), - ) - } - - private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField { - return when (id) { - ContractAddress -> screenState.contractAddressField - Network -> screenState.network - Name -> screenState.name - Symbol -> screenState.symbol - Decimals -> screenState.decimals - DerivationPath -> screenState.derivationPath - else -> throw UnsupportedOperationException() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt deleted file mode 100644 index d6c60c2b26..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.Field - -@Composable -fun BlockchainSpinner( - @StringRes title: Int, - itemList: List, - selectedItem: Field.Data, - isEnabled: Boolean = true, - textFieldConverter: (Blockchain) -> String, - dropdownItemView: @Composable ((Blockchain) -> Unit)? = null, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), - onItemSelect: (Blockchain) -> Unit, -) { - OutlinedSpinner( - modifier = Modifier.fillMaxWidth(), - label = stringResource(id = title), - itemList = itemList, - selectedItem = selectedItem, - textFieldConverter = textFieldConverter, - dropdownItemView = dropdownItemView, - isEnabled = isEnabled, - onItemSelected = onItemSelect, - closePopupTrigger = closePopupTrigger, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt deleted file mode 100644 index 453ace8c9e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.res.TangemTheme -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.DomainDialog -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -@Composable -internal fun ComposeDialogManager() { - val dialogSate = remember { mutableStateOf(null) } - val subscriber = remember { - object : StoreSubscriber { - override fun newState(state: DomainGlobalState) { - dialogSate.value = state.dialog - } - } - } - - ShowTheDialog(dialogSate) - - LaunchedEffect( - key1 = Unit, - block = { - domainStore.subscribe(subscriber) { state -> - state.skipRepeats { oldState, newState -> - oldState.globalState == newState.globalState - }.select { it.globalState } - } - }, - ) - DisposableEffect( - key1 = Unit, - effect = { - onDispose { domainStore.unsubscribe(subscriber) } - }, - ) -} - -@Composable -private fun ShowTheDialog(dialogState: MutableState) { - if (dialogState.value == null) return - - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } - - when (val dialog = dialogState.value) { - is DomainDialog.DialogError -> ErrorDialog( - title = stringResource(id = R.string.common_error), - body = errorConverter.convert(dialog.error).message, - onDismissRequest, - ) - is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) - else -> {} - } -} - -/** - * Dialog with single item selection - */ -@Composable -fun SimpleDialog( - title: String, - items: List, - onSelect: (CoinsResponse.Coin.Network) -> Unit, - onDismissRequest: () -> Unit, - itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit, -) { - Dialog( - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), - onDismissRequest = { }, - ) { - Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) { - DialogTitle(title = title) - LazyColumn { - items(items = items, key = CoinsResponse.Coin.Network::networkId) { item -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clickable { - onSelect(item) - onDismissRequest() - }, - verticalAlignment = Alignment.CenterVertically, - ) { itemContent(item) } - } - } - } - } - } -} - -@Composable -private fun DialogTitle(title: String) { - Text( - text = title, - style = LocalTextStyle.provides( - TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - ), - ).value, - ) - SpacerH16() -} - -@Composable -fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) { - AlertDialog( - title = { DialogTitle(title) }, - text = { Text(body) }, - onDismissRequest = onDismissRequest, - confirmButton = { - Button(onClick = onDismissRequest) { - Text(text = stringResource(id = R.string.common_ok)) - } - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt deleted file mode 100644 index a640e91575..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.TokenBlockchainField -import com.tangem.domain.features.addCustomToken.TokenDerivationPathField -import com.tangem.domain.features.addCustomToken.TokenField -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.OutlinedTextFieldWidget -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TokenContractAddressView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_contract_address_input_title, - placeholder = "0x0000000000000000000000000000000000000000", - isEnabled = screenFieldData.viewState.isEnabled, - isLoading = screenFieldData.viewState.isLoading, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, -// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) } - ) { - domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNameView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_name_input_title, - placeholderId = R.string.custom_token_name_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { - domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNetworkView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) - val networkField = screenFieldData.field as TokenBlockchainField - - BlockchainSpinner( - title = R.string.custom_token_network_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenSymbolView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_token_symbol_input_title, - placeholderId = R.string.custom_token_token_symbol_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDecimalsView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_decimals_input_title, - placeholder = "8", - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDerivationPathView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) - val networkField = screenFieldData.field as TokenDerivationPathField - - BlockchainSpinner( - title = R.string.custom_token_derivation_path_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - dropdownItemView = { blockchain -> - val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected - val blockchainName = state.blockchainToName(blockchain) ?: notSelected - TitleSubtitle(derivationPathName, blockchainName) - }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) } - SpacerH8() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt deleted file mode 100644 index 6aa2994210..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun HangingOverKeyboardView( - modifier: Modifier = Modifier, - keyboardState: State, - spaceBetweenKeyboard: Dp = 0.dp, - content: @Composable (BoxScope.() -> Unit), -) { - val padding = remember(keyboardState) { - when (val state = keyboardState.value) { - is Keyboard.Closed -> 0.dp - is Keyboard.Opened -> state.height + spaceBetweenKeyboard - } - } - - Box(modifier.padding(bottom = padding)) { content() } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt deleted file mode 100644 index a3ddaedf7e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import android.os.Handler -import android.os.Looper -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.os.postDelayed -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.compose.TangemTextFieldsDefault -import com.tangem.tap.common.extensions.ValueCallback - -/** -[REDACTED_AUTHOR] - */ -@Suppress("MagicNumber") -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun OutlinedSpinner( - label: String, - itemList: List, - selectedItem: Field.Data, - onItemSelected: ValueCallback, - modifier: Modifier = Modifier, - textFieldConverter: (T) -> String = { it.toString() }, - dropdownItemView: @Composable ((T) -> Unit)? = null, - isEnabled: Boolean = true, - onClose: VoidCallback = {}, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), -) { - val rIsExpanded = remember { mutableStateOf(false) } - val stateSelectedItem = remember { mutableStateOf(selectedItem.value) } - if (!selectedItem.isUserInput) { - stateSelectedItem.value = selectedItem.value - } - - val onDropDownItemSelectedInternal: (T) -> Unit = { - stateSelectedItem.value = it - rIsExpanded.value = false - onItemSelected(it) - } - val onDismissRequest = { - rIsExpanded.value = false - onClose() - } - - closePopupTrigger.close = { - onDismissRequest() - Handler(Looper.getMainLooper()).postDelayed(100) { - closePopupTrigger.onCloseComplete() - } - } - - ExposedDropdownMenuBox( - expanded = rIsExpanded.value, - onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, - ) { - OutlinedTextField( - modifier = modifier, - readOnly = true, - enabled = isEnabled, - value = textFieldConverter(stateSelectedItem.value), - onValueChange = {}, - label = { Text(label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, - colors = TangemTextFieldsDefault.defaultTextFieldColors, - ) - - if (isEnabled) { - ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) { - itemList.forEach { item -> - key(item) { - DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) { - if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item) - } - } - } - } - } - } -} - -class ClosePopupTrigger { - var close: () -> Unit = {} - var onCloseComplete: () -> Unit = {} -} - -@Preview -@Composable -private fun TestSpinnerPreview() { - OutlinedSpinner( - label = "Blockchain name", - itemList = listOf(Blockchain.values()), - selectedItem = Field.Data(Blockchain.Avalanche, false), - onItemSelected = {}, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt deleted file mode 100644 index 441969ad5e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.domain.DomainDialog -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) { - SimpleDialog( - title = stringResource(id = R.string.custom_token_network_input_title), - items = dialog.items, - onSelect = dialog.onSelect, - onDismissRequest = onDismissRequest, - ) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt deleted file mode 100644 index 3ba171f427..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.redux.domainStore - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun ContractAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53", - "ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15", - "USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1", - "Custom EVM" to "0x1111111111111111112111111111111111111113", - "Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975", - "Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -fun SolanaAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben", - "USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ", - "ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -private fun CasesListContent(casesList: List>, onItemClick: VoidCallback) { - LazyColumn( - content = { - item { - Row { - ResetContractAddressButton(onItemClick) - Text("", modifier = Modifier.weight(1f)) - ResetAllFieldsButton(onItemClick) - } - Divider() - } - items(casesList.size) { - val (info, address) = casesList[it] - ContractAddressButton(info, address, onItemClick) - } - }, - ) -} - -@Composable -fun ResetAllFieldsButton(onItemClick: VoidCallback) { - ActionButton( - name = "Reset", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) - domainStore.dispatch( - AddCustomTokenAction.OnTokenDerivationPathChanged( - Field.Data( - Blockchain.Unknown, - false, - ), - ), - ) - }, - ) -} - -@Composable -fun ResetContractAddressButton(onItemClick: VoidCallback) { - ActionButton( - name = "Set empty address", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - }, - ) -} - -@Composable -private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) { - ActionButton( - modifier = Modifier.fillMaxWidth(), - name = name, - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) - }, - ) -} - -@Composable -fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) { - Button( - modifier = modifier.padding(horizontal = 8.dp), - onClick = onClick, - ) { Text(name, fontSize = 12.sp) } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt deleted file mode 100644 index 3bae26e193..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.extensions.VoidCallback -import com.tangem.wallet.BuildConfig -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TestCasesList(onItemClick: (TestCase) -> Unit) { - if (!BuildConfig.TEST_ACTION_ENABLED) return - - Surface(color = colorResource(id = R.color.lightGray5)) { - Column(Modifier.padding(horizontal = 16.dp)) { - listOf(TestCase.ContractAddress, TestCase.SolanaTokens) - .map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) } - } - } -} - -@Composable -fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = testCase.description, - ) - Button( - onClick = onItemClick, - ) { Text("Start") } - } -} - -enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) { - ContractAddress("Test contract address field", { ContractAddressTests(it) }), - Auto("Test contract address field", { ContractAddressTests(it) }), - SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }), - ; -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt b/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt new file mode 100644 index 0000000000..afb3010ad3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.details + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +class DarkThemeFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) { + val isDarkThemeEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "DARK_THEME_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt new file mode 100644 index 0000000000..2ed04a4c94 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.details.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultDetailsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : DetailsFeatureToggles { + + override val isRedesignedAppCurrencySelectorEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt new file mode 100644 index 0000000000..dd1cd3bbdd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.details.featuretoggles + +interface DetailsFeatureToggles { + + val isRedesignedAppCurrencySelectorEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt new file mode 100644 index 0000000000..2e6fd9874a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.features.details.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object DetailsFeatureTogglesModule { + + @Provides + fun provideDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles { + return DefaultDetailsFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file 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 1248e2537c..338e8f255c 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 @@ -1,7 +1,8 @@ package com.tangem.tap.features.details.redux import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.blockchain.common.Wallet +import com.tangem.core.ui.extensions.TextReference +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 @@ -12,7 +13,8 @@ sealed class DetailsAction : Action { data class PrepareScreen( val scanResponse: ScanResponse, - val wallets: List, + val darkThemeSwitchEnabled: Boolean, + val shouldSaveUserWallets: Boolean, ) : DetailsAction() object ReCreateTwinsWallet : DetailsAction() @@ -20,7 +22,8 @@ sealed class DetailsAction : Action { sealed class ResetToFactory : DetailsAction() { object Start : ResetToFactory() object Proceed : ResetToFactory() - data class Confirm(val confirmed: Boolean) : ResetToFactory() + data class AcceptCondition1(val accepted: Boolean) : ResetToFactory() + data class AcceptCondition2(val accepted: Boolean) : ResetToFactory() object Failure : ResetToFactory() object Success : ResetToFactory() } @@ -29,6 +32,14 @@ sealed class DetailsAction : Action { data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() object ResetCardSettingsData : DetailsAction() + object ScanAndSaveUserWallet : DetailsAction() { + + object Success : DetailsAction() + + data class Error(val error: TextReference?) : DetailsAction() + } + + object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() @@ -64,14 +75,25 @@ sealed class DetailsAction : Action { } data class CheckBiometricsStatus( - val awaitStatusChange: Boolean, - val lifecycleCoroutineScope: LifecycleCoroutineScope, + val lifecycleScope: LifecycleCoroutineScope, ) : AppSettings() object EnrollBiometrics : AppSettings() data class BiometricsStatusChanged( val needEnrollBiometrics: Boolean, ) : AppSettings() + + data class ChangeAppThemeMode( + val appThemeMode: AppThemeMode, + ) : AppSettings() + + data class ChangeBalanceHiding( + val hideBalance: Boolean, + ) : AppSettings() + + data class ChangeAppCurrency( + val fiatCurrency: FiatCurrency, + ) : AppSettings() } data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : 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 a672117ab2..3dac94f996 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 @@ -1,15 +1,19 @@ package com.tangem.tap.features.details.redux import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.common.CompletionResult +import com.tangem.common.* +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.guard -import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.extensions.TextReference +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.balancehiding.BalanceHidingSettings import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse @@ -19,6 +23,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -33,8 +38,10 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.tangemSdkManager +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -47,7 +54,7 @@ import timber.log.Timber class DetailsMiddleware { private val eraseWalletMiddleware = EraseWalletMiddleware() private val manageSecurityMiddleware = ManageSecurityMiddleware() - private val managePrivacyMiddleware = ManagePrivacyMiddleware() + private val appSettingsMiddleware = AppSettingsMiddleware() private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware() val detailsMiddleware: Middleware = { _, stateProvider -> { next -> @@ -67,43 +74,14 @@ class DetailsMiddleware { when (action) { is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state) - is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) + is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) - DetailsAction.ScanCard -> { - scope.launch { - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) - .scan(allowsRequestAccessCodeFromRepository = true) - .doOnSuccess { scanResponse -> - // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards - store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - val currentUserWalletId = state.scanResponse - ?.let { UserWalletIdBuilder.scanResponse(it).build() } - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) - .build() - val isSameWallet = currentUserWalletId == scannedUserWalletId - - if (isSameWallet) { - store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), - ) - } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) - } - } - } - } + is DetailsAction.ScanCard -> scanCard(state) + is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet() } } @@ -124,6 +102,27 @@ class DetailsMiddleware { scope.launch { val userWalletId = UserWalletIdBuilder.card(card).build() + // we must require a password regardless of biometric settings + val policy = tangemSdkManager.userCodeRequestPolicy + val doBeforeErase = { + val type = if (card.isAccessCodeSet) { + UserCodeType.AccessCode + } else if (card.isPasscodeSet == true) { + UserCodeType.Passcode + } else { + null + } + + type?.let { + tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type)) + } + } + + val doAfterErase = { + tangemSdkManager.setUserCodeRequestPolicy(policy) + } + + doBeforeErase() tangemSdkManager.resetToFactorySettings(card.cardId, true) .flatMap { userWalletsListManager.delete(listOfNotNull(userWalletId)) } .flatMap { tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) } @@ -147,6 +146,9 @@ class DetailsMiddleware { Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) } } + .doOnResult { + doAfterErase() + } } } else -> Unit @@ -207,7 +209,10 @@ class DetailsMiddleware { } } - class ManagePrivacyMiddleware { + class AppSettingsMiddleware { + + private val checkBiometricsStatusJobHolder = JobHolder() + fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -217,15 +222,22 @@ class DetailsMiddleware { } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { - checkBiometricsStatus( - awaitStatusChange = action.awaitStatusChange, - state = state, - lifecycleScope = action.lifecycleCoroutineScope, - ) + observeBiometricsStatusChanges(state, action.lifecycleScope) } is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() } + is DetailsAction.AppSettings.ChangeAppThemeMode -> { + changeAppThemeMode(action.appThemeMode) + } + is DetailsAction.AppSettings.ChangeBalanceHiding -> { + changeBalanceHiding(action.hideBalance) + } + is DetailsAction.AppSettings.ChangeAppCurrency -> { + store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency)) + } is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, @@ -233,27 +245,20 @@ class DetailsMiddleware { } } - /** - * @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100 - * milliseconds until it changes - * */ - private fun checkBiometricsStatus( - awaitStatusChange: Boolean, - state: DetailsState, - lifecycleScope: LifecycleCoroutineScope, - ) { - lifecycleScope.launch { - if (awaitStatusChange) { - while (state.appSettingsState.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) { - delay(timeMillis = 100) + private fun observeBiometricsStatusChanges(state: DetailsState, lifecycleScope: LifecycleCoroutineScope) { + lifecycleScope.launch(Dispatchers.IO) { + do { + val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() + + if (needEnrollBiometrics != null && + needEnrollBiometrics != state.appSettingsState.needEnrollBiometrics + ) { + store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics)) } - } - store.dispatchWithMain( - DetailsAction.AppSettings.BiometricsStatusChanged( - needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics, - ), - ) - } + + delay(timeMillis = 500) + } while (true) + }.saveIn(checkBiometricsStatusJobHolder) } private fun enrollBiometrics() { @@ -261,9 +266,33 @@ class DetailsMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } + private fun changeAppThemeMode(appThemeMode: AppThemeMode) { + val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository) + + scope.launch { + repository.changeAppThemeMode(appThemeMode) + + store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode)) + } + } + + private fun changeBalanceHiding(hideBalance: Boolean) { + val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository) + + scope.launch { + val newState = BalanceHidingSettings( + isHidingEnabledInSettings = hideBalance, + isBalanceHidden = false, + ) + + repository.storeBalanceHidingSettings(newState) + } + } + private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change - if (preferencesStorage.shouldSaveUserWallets == enable) { + val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + if (walletsRepository.shouldSaveUserWalletsSync() == enable) { store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) return@launch } @@ -351,7 +380,8 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldShowSaveUserWalletScreen = false - preferencesStorage.shouldSaveUserWallets = true + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = true) store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) } @@ -367,7 +397,8 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) deleteSavedAccessCodes() updateUserWalletsListManager(enableUserWalletsSaving = false) - preferencesStorage.shouldSaveUserWallets = false + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = false) store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true)) store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) @@ -419,10 +450,7 @@ class DetailsMiddleware { return null } - return UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + return UserWalletsListManager.provideBiometricImplementation(context) } } @@ -455,4 +483,97 @@ class DetailsMiddleware { } } } + + private fun scanCard(state: DetailsState) = scope.launch { + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { scanResponse -> + // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + val currentUserWalletId = state.scanResponse + ?.let { UserWalletIdBuilder.scanResponse(it).build() } + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) + .build() + val isSameWallet = currentUserWalletId == scannedUserWalletId + + if (isSameWallet) { + store.dispatchOnMain( + DetailsAction.PrepareCardSettingsData( + scanResponse.card, + scanResponse.cardTypesResolver, + ), + ) + } else { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.error_wrong_wallet_tapped, + ), + ) + } + } + } + + private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() + + // Update access code policy for access code saving when a card was scanned + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) + + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), + onWalletNotCreated = { + // No need to rollback policy, continue with the policy set before the card scan + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + }, + disclaimerWillShow = { + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + onSuccess = { scanResponse -> + saveUserWalletAndPopBackToWalletScreen(scanResponse) + .doOnFailure { error -> + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to save user wallet") + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + } + }, + onFailure = { error -> + // Rollback policy if card scanning was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to scan card") + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + }, + ) + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { + val userWallet = UserWalletBuilder(scanResponse).build() + ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + + return userWalletsListManager.save(userWallet) + .doOnSuccess { + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + store.onUserWalletSelected(userWallet) + } + } + } + + private fun TangemError.toTextReference(): TextReference? { + if (silent) return null + + return messageResId?.let(::resourceReference) ?: stringReference(customMessage) + } } \ No newline at end of file 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 e28b9cd398..ff94b5cc7a 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux +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 @@ -8,13 +9,16 @@ import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.preferencesStorage import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.runBlocking import org.rekotlin.Action -import java.util.* +import java.util.EnumSet object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) } +@Suppress("CyclomaticComplexMethod") private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState val detailsState = state.detailsState @@ -39,9 +43,25 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.AppSettings -> { handlePrivacyAction(action, detailsState) } - is DetailsAction.ChangeAppCurrency -> - detailsState.copy(appCurrency = action.fiatCurrency) + is DetailsAction.ChangeAppCurrency -> detailsState.copy( + appSettingsState = detailsState.appSettingsState.copy( + selectedFiatCurrency = action.fiatCurrency, + ), + ) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) + is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy( + isScanningInProgress = true, + ) + is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy( + isScanningInProgress = false, + error = action.error, + ) + is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy( + isScanningInProgress = false, + ) + is DetailsAction.DismissError -> detailsState.copy( + error = null, + ) else -> detailsState } } @@ -49,13 +69,21 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { return DetailsState( scanResponse = action.scanResponse, - wallets = action.wallets, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, - appCurrency = store.state.globalState.appCurrency, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, - saveWallets = preferencesStorage.shouldSaveUserWallets, + saveWallets = action.shouldSaveUserWallets, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, + selectedFiatCurrency = store.state.globalState.appCurrency, + selectedThemeMode = runBlocking { + store.state.daggerGraphState + .get { appThemeModeRepository }.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT + }, + isHidingEnabled = runBlocking { + store.state.daggerGraphState + .get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings + }, + darkThemeSwitchEnabled = action.darkThemeSwitchEnabled, ), ) } @@ -70,6 +98,9 @@ private fun handlePrepareCardSettingsScreen( manageSecurityState = prepareSecurityOptions(card, cardTypesResolver), card = card, resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver), + resetButtonEnabled = false, + condition1Checked = false, + condition2Checked = false, accessCodeRecovery = if (cardTypesResolver.isWallet2()) { val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false AccessCodeRecoveryState( @@ -122,8 +153,25 @@ private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: Card private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState { return when (action) { - is DetailsAction.ResetToFactory.Confirm -> - state.copy(cardSettingsState = state.cardSettingsState?.copy(resetConfirmed = action.confirmed)) + is DetailsAction.ResetToFactory.AcceptCondition1 -> { + val warning1Checked = action.accepted + state.copy( + cardSettingsState = state.cardSettingsState?.copy( + condition1Checked = action.accepted, + resetButtonEnabled = warning1Checked && state.cardSettingsState.condition2Checked, + ), + ) + } + is DetailsAction.ResetToFactory.AcceptCondition2 -> { + val warning2Checked = action.accepted + state.copy( + cardSettingsState = state.cardSettingsState?.copy( + condition2Checked = action.accepted, + resetButtonEnabled = warning2Checked && state.cardSettingsState.condition1Checked, + ), + ) + } + else -> state } } @@ -194,6 +242,21 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail needEnrollBiometrics = action.needEnrollBiometrics, ), ) + is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy( + appSettingsState = state.appSettingsState.copy( + selectedThemeMode = action.appThemeMode, + ), + ) + is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy( + appSettingsState = state.appSettingsState.copy( + selectedFiatCurrency = action.fiatCurrency, + ), + ) + is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy( + appSettingsState = state.appSettingsState.copy( + isHidingEnabled = action.hideBalance, + ), + ) is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.CheckBiometricsStatus, -> state 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 6fd17a4119..3e49b20905 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 @@ -1,20 +1,21 @@ package com.tangem.tap.features.details.redux -import com.tangem.blockchain.common.Wallet +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType -import java.util.* +import java.util.EnumSet data class DetailsState( val scanResponse: ScanResponse? = null, - val wallets: List = emptyList(), val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, - val appCurrency: FiatCurrency = FiatCurrency.Default, + val isScanningInProgress: Boolean = false, + val error: TextReference? = null, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType @@ -40,7 +41,9 @@ data class CardSettingsState( val card: CardDTO, val manageSecurityState: ManageSecurityState?, val resetCardAllowed: Boolean, - val resetConfirmed: Boolean = false, + val resetButtonEnabled: Boolean, + val condition1Checked: Boolean, + val condition2Checked: Boolean, val accessCodeRecovery: AccessCodeRecoveryState? = null, ) @@ -56,7 +59,11 @@ data class AppSettingsState( val saveAccessCodes: Boolean = false, val isBiometricsAvailable: Boolean = false, val needEnrollBiometrics: Boolean = false, + val isHidingEnabled: Boolean = false, val isInProgress: Boolean = false, + val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default, + val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT, + val darkThemeSwitchEnabled: Boolean = false, ) enum class SecurityOption { LongTap, PassCode, AccessCode } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index a436d55d8e..4ed140a246 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -14,6 +14,8 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.walletconnect.WalletConnectActions +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -32,6 +34,7 @@ import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -59,6 +62,28 @@ class WalletConnectMiddleware { if (DemoHelper.tryHandle(state, action)) return when (action) { + is WalletConnectActions.New.Initialize -> { + val userWallet = action.userWallet + val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) { + userWallet.cardId + } else { // if wallet has backup, any card from wallet can be used to sign + null + } + scope.launch { + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch + wcInteractor.startListening( + userWalletId = userWallet.walletId.stringValue, + cardId = cardId, + ) + } + } + is WalletConnectActions.New.SetupUserChains -> { + scope.launch { + val userWallet = action.userWallet + val wcInteractor = store.state.daggerGraphState.walletConnectInteractor ?: return@launch + wcInteractor.setUserChains(getAccountsForWc(wcInteractor, userWallet)) + } + } is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager() is WalletConnectAction.RestoreSessions -> { walletConnectManager.restoreSessions(action.scanResponse) @@ -246,47 +271,50 @@ class WalletConnectMiddleware { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) return } - val walletManager = getWalletManager( - wallet = action.session.wallet, - blockchain = blockchain, - walletState = store.state.walletState, - ).guard { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.AddNetwork(blockchain.fullName), - ), + scope.launch { + val walletManager = getWalletManager( + wallet = action.session.wallet, + blockchain = blockchain, + walletState = store.state.walletState, + ).guard { + store.dispatchOnMain( + GlobalAction.ShowDialog( + WalletConnectDialog.AddNetwork(blockchain.fullName), + ), + ) + return@launch + } + val updatedWallet = action.session.wallet.copy( + walletPublicKey = walletManager.wallet.publicKey.seedKey, + derivedPublicKey = walletManager.wallet.publicKey.derivedKey, + derivationPath = walletManager.wallet.publicKey.derivationPath, + blockchain = action.blockchain, ) - return + val updatedSession = action.session.copy(wallet = updatedWallet) + store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) } - val updatedWallet = action.session.wallet.copy( - walletPublicKey = walletManager.wallet.publicKey.seedKey, - derivedPublicKey = walletManager.wallet.publicKey.derivedKey, - derivationPath = walletManager.wallet.publicKey.derivationPath, - blockchain = action.blockchain, - ) - val updatedSession = action.session.copy(wallet = updatedWallet) - store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession)) } is WalletConnectAction.UpdateBlockchain -> { walletConnectManager.updateBlockchain(action.updatedSession) } is WalletConnectAction.ApproveProposal -> { - val accounts = store.state.walletState.walletManagers - .mapNotNull { - val wallet = it.wallet - val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), - ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, + scope.launch { + val accounts = getWalletManagers() + .mapNotNull { + val wallet = it.wallet + val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } } - } - - walletConnectInteractor.approveSessionProposal(accounts) + walletConnectInteractor.approveSessionProposal(accounts) + } } is WalletConnectAction.RejectProposal -> { walletConnectInteractor.rejectSessionProposal() @@ -345,6 +373,19 @@ class WalletConnectMiddleware { } } + private suspend fun getWalletManagers(): List { + val walletManagerToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + return if (walletManagerToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList() + walletManagerFacade.getStoredWalletManagers(userWallet.walletId) + } else { + store.state.walletState.walletManagers + } + } + private fun scanCard(scanResponse: ScanResponse, session: WalletConnectSession, chainId: Int?) { val blockchain = WalletConnectNetworkUtils.parseBlockchain( chainId = chainId, @@ -430,7 +471,7 @@ class WalletConnectMiddleware { ) } - private fun getWalletManager( + private suspend fun getWalletManager( wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState, @@ -440,18 +481,50 @@ class WalletConnectMiddleware { } else { blockchain } + val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null val derivation = blockchainToMake.derivationPath( - style = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle(), + style = userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), )?.rawPath - val blockchainNetwork = BlockchainNetwork( - blockchain = blockchainToMake, - derivationPath = derivation, - tokens = emptyList(), - ) - return walletState.getWalletManager(blockchainNetwork) + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + walletManagerFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + blockchain = blockchainToMake, + derivationPath = derivation, + ) + } else { + val blockchainNetwork = BlockchainNetwork( + blockchain = blockchainToMake, + derivationPath = derivation, + tokens = emptyList(), + ) + walletState.getWalletManager(blockchainNetwork) + } } private fun isWalletConnectUri(uri: String): Boolean { return WalletConnectManager.isCorrectWcUri(uri) || walletConnectInteractor.isWalletConnectUri(uri) } + + private suspend fun getAccountsForWc(wcInteractor: WalletConnectInteractor, userWallet: UserWallet): List { + val walletManagerFacade = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + return walletManagerFacade.getStoredWalletManagers(userWallet.walletId).mapNotNull { + val wallet = it.wallet + val chainId = wcInteractor.blockchainHelper.networkIdToChainIdOrNull( + wallet.blockchain.toNetworkId(), + ) + chainId?.let { + Account( + chainId, + wallet.address, + wallet.publicKey.derivationPath?.rawPath, + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt new file mode 100644 index 0000000000..b9273ef99b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.details.ui.appcurrency + +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 +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class AppCurrencySelectorFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + private val viewModel: AppCurrencySelectorViewModel by viewModels() + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + AppCurrencySelectorScreen( + modifier = modifier, + state = uiState, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt new file mode 100644 index 0000000000..b6d25d03a0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.details.ui.appcurrency + +internal interface AppCurrencySelectorIntents { + + fun onBackClick() + + fun onSearchClick() + + fun onSearchInputChange(input: String) + + fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) + + fun onDismissSearchClick() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt new file mode 100644 index 0000000000..a38cac5c0a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -0,0 +1,363 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency +import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier: Modifier = Modifier) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TopBar( + modifier = Modifier.fillMaxWidth(), + scrollBehavior = scrollBehavior, + state = state, + ) + }, + content = { paddingValues -> + val contentModifier = Modifier + .padding(paddingValues) + .fillMaxSize() + + when (state) { + is AppCurrencySelectorState.Loading -> LoadingList( + modifier = contentModifier, + ) + is AppCurrencySelectorState.Content -> CurrenciesList( + modifier = contentModifier, + listState = listState, + currencies = state.items, + selectedId = state.selectedId.orEmpty(), + onCurrencyClick = state.onCurrencyClick, + ) + } + }, + ) + + if (state is AppCurrencySelectorState.Content) { + EventEffect(event = state.scrollToSelected) { + listState.scrollToItem(it) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + state: AppCurrencySelectorState, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = TopAppBarColors, + navigationIcon = { + IconButton( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size32), + onClick = state.onBackClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + ) + } + }, + title = { + when (state) { + is AppCurrencySelectorState.Search -> SearchBar( + modifier = Modifier.fillMaxWidth(), + onInputChange = state.onSearchInputChange, + ) + is AppCurrencySelectorState.Loading, + is AppCurrencySelectorState.Default, + -> Text( + text = stringResource(id = R.string.details_row_title_currency), + style = TangemTheme.typography.subtitle1, + ) + } + }, + actions = { + when (state) { + is AppCurrencySelectorState.Content -> { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = state.onTopBarActionClick, + ) { + val iconResId = when (state) { + is AppCurrencySelectorState.Default -> R.drawable.ic_search_24 + is AppCurrencySelectorState.Search -> R.drawable.ic_close_24 + } + val iconTint = when (state) { + is AppCurrencySelectorState.Default -> TangemTheme.colors.icon.primary1 + is AppCurrencySelectorState.Search -> TangemTheme.colors.icon.informative + } + + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = iconResId), + tint = iconTint, + contentDescription = null, + ) + } + } + is AppCurrencySelectorState.Loading -> Unit + } + SpacerW(width = TangemTheme.dimens.spacing8) + }, + ) +} + +@Composable +private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modifier) { + val focusRequester = remember { FocusRequester() } + var input by remember { mutableStateOf(value = "") } + + TextField( + modifier = modifier + .focusRequester(focusRequester), + value = input, + onValueChange = { input = it }, + singleLine = true, + textStyle = TangemTheme.typography.subtitle2, + placeholder = { + Text(text = stringResource(id = R.string.common_search)) + }, + colors = SearchBarColors, + ) + + LaunchedEffect(key1 = input) { + onInputChange(input) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun LoadingList(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + repeat(times = 10) { + Row( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .height(TangemTheme.dimens.size56) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size24)) + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size24) + .fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun CurrenciesList( + listState: LazyListState, + currencies: ImmutableList, + selectedId: String, + onCurrencyClick: (Currency) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + state = listState, + ) { + items( + items = currencies, + key = Currency::id, + ) { currency -> + val onClick = remember(key1 = currency) { + { onCurrencyClick(currency) } + } + + CurrencyItem( + modifier = Modifier.fillMaxWidth(), + name = currency.name, + isSelected = currency.id == selectedId, + onClick = onClick, + ) + } + } +} + +@Composable +private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .heightIn(min = TangemTheme.dimens.size56), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + RadioButton( + modifier = Modifier.size(TangemTheme.dimens.size24), + selected = isSelected, + onClick = onClick, + interactionSource = interactionSource, + colors = RadioButtonColors, + ) + Text( + text = name, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private val TopAppBarColors: TopAppBarColors + @Composable + get() = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + // Currently (08.09.23) it's not working when scrolling programmatically + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ) + +private val SearchBarColors: TextFieldColors + @Composable + get() = TextFieldDefaults.colors( + unfocusedContainerColor = TangemTheme.colors.background.secondary, + focusedContainerColor = TangemTheme.colors.background.secondary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.secondary, + focusedPlaceholderColor = TangemTheme.colors.text.disabled, + unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + focusedIndicatorColor = TangemTheme.colors.background.secondary, + unfocusedIndicatorColor = TangemTheme.colors.background.secondary, + cursorColor = TangemTheme.colors.icon.primary1, + ) + +private val RadioButtonColors: RadioButtonColors + @Composable + get() = RadioButtonDefaults.colors( + selectedColor = TangemTheme.colors.icon.accent, + unselectedColor = TangemTheme.colors.icon.secondary, + ) + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Light( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = false) { + AppCurrencySelectorScreen(param) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Dark( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = true) { + AppCurrencySelectorScreen(param) + } +} + +private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val items = listOf( + "US Dollar (USD) – $", + "Inited Arab Emirates Dirham (AED) – DH", + "Argentine Peso (ARS) – $", + "Australian Dollar (AUD) – A$", + "Bangladeshi Taka (BDT) – ৳", + "Bahraini Dinar (BHD) – BD", + "Bermudian Dollar (BMD) – $", + "Brazil Real (BRL) – R$", + "Canadian Dollar (CAD) – CA$", + "Swiss Franc (CHF) – Fr", + "Chilean Peso (CLP) – CLP$", + "Chinese Yan (CNY)", + ) + .mapIndexed { index, s -> Currency(index.toString(), s) } + .toPersistentList() + + AppCurrencySelectorState.Loading(onBackClick = {}).let(::add) + AppCurrencySelectorState.Default( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onTopBarActionClick = {}, + ).let(::add) + AppCurrencySelectorState.Search( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onSearchInputChange = {}, + onTopBarActionClick = {}, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt new file mode 100644 index 0000000000..b3b606c396 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed class AppCurrencySelectorState { + + abstract val onBackClick: () -> Unit + + data class Loading( + override val onBackClick: () -> Unit, + ) : AppCurrencySelectorState() + + @Immutable + sealed class Content : AppCurrencySelectorState() { + abstract val selectedId: String + abstract val scrollToSelected: StateEvent + abstract val items: PersistentList + abstract val onCurrencyClick: (Currency) -> Unit + abstract val onTopBarActionClick: () -> Unit + + fun copySealed( + selectedId: String? = this.selectedId, + items: PersistentList = this.items, + scrollToSelected: StateEvent = this.scrollToSelected, + onCurrencyClick: (Currency) -> Unit = this.onCurrencyClick, + onTopBarActionClick: () -> Unit = this.onTopBarActionClick, + ): AppCurrencySelectorState = when (this) { + is Default -> this.copy( + selectedId = selectedId.orEmpty(), + items = items, + scrollToSelected = scrollToSelected, + onCurrencyClick = onCurrencyClick, + onTopBarActionClick = onTopBarActionClick, + ) + is Search -> this.copy( + selectedId = selectedId.orEmpty(), + items = items, + scrollToSelected = scrollToSelected, + onCurrencyClick = onCurrencyClick, + onTopBarActionClick = onTopBarActionClick, + ) + } + } + + data class Default( + override val selectedId: String, + override val items: PersistentList, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + override val scrollToSelected: StateEvent, + ) : Content() + + data class Search( + override val selectedId: String, + override val items: PersistentList, + override val scrollToSelected: StateEvent, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + val onSearchInputChange: (String) -> Unit, + ) : Content() + + data class Currency( + val id: String, + val name: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt new file mode 100644 index 0000000000..6f091f4376 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt @@ -0,0 +1,125 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.tap.features.details.ui.appcurrency.converter.CurrencyConverter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +internal class AppCurrencySelectorStateHolder( + private val intents: AppCurrencySelectorIntents, + private val onSubscription: () -> Unit, + stateFlowScope: CoroutineScope, +) { + + private var availableCurrencies: PersistentList = persistentListOf() + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val currencyConverter by lazy(mode = LazyThreadSafetyMode.NONE) { CurrencyConverter() } + + val stateFlow: StateFlow = stateFlowInternal + .onSubscription { onSubscription() } + .stateIn( + scope = stateFlowScope, + started = SharingStarted.WhileSubscribed(), + initialValue = getInitialState(), + ) + + fun updateStateWithAvailableCurrencies(currencies: List) { + availableCurrencies = currencyConverter.convertList(currencies).toPersistentList() + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> getContentState(availableCurrencies) + is AppCurrencySelectorState.Content -> copySealed(items = availableCurrencies) + } + } + } + + fun updateStateWithSelectedCurrency(selectedCurrency: AppCurrency, selectedCurrencyIndex: Int) { + val selectedCurrencyId = selectedCurrency.code + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Content -> copySealed( + selectedId = selectedCurrencyId, + scrollToSelected = triggeredEvent(selectedCurrencyIndex, ::consumeScrollToSelectedEvent), + ) + } + } + } + + fun updateStateWithSearch(input: String = "") { + val filteredItems = availableCurrencies.mutate { list -> + list.removeAll { input.lowercase() !in it.name.lowercase() } + } + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Search -> copy(items = filteredItems) + is AppCurrencySelectorState.Default -> getSearchState(filteredItems, selectedId) + } + } + } + + fun updateStateWithoutSearch() { + updateState { + when (this) { + is AppCurrencySelectorState.Search -> getContentState(availableCurrencies, selectedId) + is AppCurrencySelectorState.Default, + is AppCurrencySelectorState.Loading, + -> this + } + } + } + + private fun getInitialState() = AppCurrencySelectorState.Loading( + onBackClick = intents::onBackClick, + ) + + private fun getContentState( + items: PersistentList, + selectedCurrencyId: String = "", + ) = AppCurrencySelectorState.Default( + selectedId = selectedCurrencyId, + items = items, + onBackClick = intents::onBackClick, + onCurrencyClick = intents::onCurrencyClick, + onTopBarActionClick = intents::onSearchClick, + scrollToSelected = consumedEvent(), + ) + + private fun getSearchState( + filteredItems: PersistentList, + selectedCurrencyId: String, + ) = AppCurrencySelectorState.Search( + selectedId = selectedCurrencyId, + items = filteredItems, + scrollToSelected = consumedEvent(), + onBackClick = intents::onBackClick, + onCurrencyClick = intents::onCurrencyClick, + onSearchInputChange = intents::onSearchInputChange, + onTopBarActionClick = intents::onDismissSearchClick, + ) + + private inline fun updateState(block: AppCurrencySelectorState.() -> AppCurrencySelectorState) { + stateFlowInternal.update(block) + } + + private fun consumeScrollToSelectedEvent() { + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Content -> copySealed(scrollToSelected = consumedEvent()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt new file mode 100644 index 0000000000..7b07e7d471 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase +import com.tangem.tap.common.analytics.events.Settings +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class AppCurrencySelectorViewModel @Inject constructor( + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase, + private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase, + private val reduxNavController: ReduxNavController, + private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel(), AppCurrencySelectorIntents { + + private val stateController = AppCurrencySelectorStateHolder( + intents = this, + onSubscription = { fetchCurrencies() }, + stateFlowScope = viewModelScope, + ) + + val uiState: StateFlow = stateController.stateFlow + + override fun onBackClick() { + reduxNavController.popBackStack() + } + + override fun onSearchClick() { + stateController.updateStateWithSearch() + } + + override fun onSearchInputChange(input: String) { + stateController.updateStateWithSearch(input) + } + + override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) { + viewModelScope.launch(dispatchers.io) { + selectAppCurrencyUseCase(currency.id) + .onRight { + analyticsEventHandler.send( + event = Settings.AppSettings.MainCurrencyChanged(currencyType = currency.name), + ) + reduxNavController.popBackStack() + } + } + } + + override fun onDismissSearchClick() { + stateController.updateStateWithoutSearch() + } + + private fun fetchCurrencies() { + viewModelScope.launch(dispatchers.io) { + val availableCurrencies = getAvailableCurrenciesUseCase() + .onRight(stateController::updateStateWithAvailableCurrencies) + .getOrNull() + + getSelectedAppCurrencyUseCase().collectLatest { maybeSelectedCurrency -> + val selectedCurrency = maybeSelectedCurrency.getOrNull() ?: return@collectLatest + val selectedCurrencyIndex = availableCurrencies?.indexOfFirst { it == selectedCurrency } + + if (selectedCurrencyIndex != null && selectedCurrencyIndex != -1) { + stateController.updateStateWithSelectedCurrency(selectedCurrency, selectedCurrencyIndex) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt new file mode 100644 index 0000000000..445f24d3c8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.details.ui.appcurrency.converter + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState +import com.tangem.utils.converter.Converter + +internal class CurrencyConverter : Converter { + + override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency { + val fullCurrencyName = with(value) { "$name ($code) — $symbol" } + + return AppCurrencySelectorState.Currency(value.code, fullCurrencyName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt new file mode 100644 index 0000000000..43e66b065a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +internal class AppSettingsDialogsFactory { + + fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createThemeModeSelectorDialog( + selectedModeIndex: Int, + onSelect: (AppThemeMode) -> Unit, + onDismiss: () -> Unit, + ): Dialog.Selector { + val modes = AppThemeMode.available + + return Dialog.Selector( + title = resourceReference(R.string.app_settings_theme_selector_title), + selectedItemIndex = selectedModeIndex, + items = modes.map { mode -> + resourceReference( + id = when (mode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ) + }.toImmutableList(), + onSelect = { index -> + val mode = AppThemeMode.available[index] + + onSelect(mode) + }, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 2be6f7b037..122e9437d1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -1,51 +1,51 @@ package com.tangem.tap.features.details.ui.appsettings -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope -import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store -import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class AppSettingsFragment : Fragment(), StoreSubscriber { - private val viewModel = AppSettingsViewModel(store) - private var screenState: MutableState = - mutableStateOf(viewModel.updateState(store.state.detailsState)) +@AndroidEntryPoint +internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) - viewModel.checkBiometricsStatus(lifecycleScope) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Inject + lateinit var detailsFeatureToggles: DetailsFeatureToggles + + @Inject + lateinit var appCurrencyRepository: AppCurrencyRepository + + private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) { + AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - AppSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + AppSettingsScreen( + modifier = modifier, + state = viewModel.uiState, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + + override fun onResume() { + super.onResume() + viewModel.checkBiometricsStatus(lifecycleScope) } override fun onStart() { @@ -57,11 +57,6 @@ class AppSettingsFragment : Fragment(), StoreSubscriber { } } - override fun onResume() { - super.onResume() - viewModel.refreshBiometricsStatus(lifecycleScope) - } - override fun onStop() { super.onStop() store.unsubscribe(this) @@ -69,6 +64,6 @@ class AppSettingsFragment : Fragment(), StoreSubscriber { override fun newState(state: DetailsState) { if (activity == null || view == null) return - screenState.value = viewModel.updateState(state) + viewModel.updateState(state) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt new file mode 100644 index 0000000000..0d1f66f8bb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -0,0 +1,91 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.wallet.R + +internal class AppSettingsItemsFactory { + + fun createEnrollBiometricsCard(onClick: () -> Unit): Item.Card { + return Item.Card( + id = "enroll_biometrics_card", + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference(R.string.app_settings_enable_biometrics_description), + iconResId = R.drawable.ic_alert_circle_24, + onClick = onClick, + ) + } + + fun createSaveWalletsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_wallets_switch", + title = resourceReference(R.string.app_settings_saved_wallet), + description = resourceReference(R.string.app_settings_saved_wallet_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createSaveAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_access_codes_switch", + title = resourceReference(R.string.app_settings_saved_access_codes), + description = resourceReference(R.string.app_settings_saved_access_codes_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createFlipToHideBalanceSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "flip_to_hide_balance_switch", + title = resourceReference(R.string.details_row_title_flip_to_hide), + description = resourceReference(R.string.details_row_description_flip_to_hide), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_app_currency_button", + title = resourceReference(R.string.details_row_title_currency), + description = stringReference(currentAppCurrencyName), + isEnabled = true, + onClick = onClick, + ) + } + + fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_theme_mode_button", + title = resourceReference(R.string.app_settings_theme_selector_title), + description = resourceReference( + id = when (currentThemeMode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ), + isEnabled = true, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 9255526f4a..7c99ed8b24 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,233 +1,110 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW32 +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.redux.AppSetting -import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard -import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.appsettings.components.* import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold -import com.tangem.tap.features.details.ui.common.TangemSwitch import com.tangem.wallet.R +import kotlinx.collections.immutable.persistentListOf @Composable -fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) { +internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( - content = { AppSettings(state = state) }, + modifier = modifier, + content = { + when (state) { + is AppSettingsScreenState.Content -> AppSettings(state = state) + is AppSettingsScreenState.Loading -> Unit + } + }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, ) } @Composable -private fun AppSettings(state: AppSettingsScreenState) { - var dialogType by remember { mutableStateOf(null) } - val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it } - - dialogType?.let { - SettingsAlertDialog( - element = it, - onDialogStateChange = onDialogStateChange, - onSettingToggle = { state.onSettingToggled(it, false) }, - ) +private fun AppSettings(state: AppSettingsScreenState.Content) { + val dialog by rememberUpdatedState(newValue = state.dialog) + when (val safeDialog = dialog) { + is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog) + is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog) + null -> Unit } - Column(modifier = Modifier.fillMaxSize()) { - if (state.showEnrollBiometricsCard) { - EnrollBiometricsCard(onClick = state.onEnrollBiometrics) - SpacerH24() - } - - AppSettingsElement( - state = state, - setting = AppSetting.SaveWallets, - onDialogStateChange = onDialogStateChange, - ) - SpacerH32() - AppSettingsElement( - state = state, - setting = AppSetting.SaveAccessCode, - onDialogStateChange = onDialogStateChange, - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun AppSettingsElement( - state: AppSettingsScreenState, - setting: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, -) { - val titleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes - } - val subtitleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer - } - val checked = state.settings[setting] ?: false - - val titleTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.secondary - }, - ) - val descriptionTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.secondary - } else { - TangemTheme.colors.text.tertiary - }, - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing20), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier.weight(weight = .9f), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = titleRes), - style = TangemTheme.typography.subtitle1, - color = titleTextColor, - ) - SpacerH4() - Text( - text = stringResource(id = subtitleRes), - style = TangemTheme.typography.body2, - color = descriptionTextColor, - ) - } - SpacerW32() - TangemSwitch( - checked = checked, - enabled = state.isTogglesEnabled, - onCheckedChange = { isChecked -> - onCheckedChange( - element = setting, - enabled = isChecked, - onSettingToggled = state.onSettingToggled, - onDialogStateChange = onDialogStateChange, + LazyColumn { + items( + items = state.items, + key = Item::id, + ) { item -> + when (item) { + is Item.Card -> SettingsCardItem( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + item = item, ) - }, - ) - } -} - -private fun onCheckedChange( - element: AppSetting, - enabled: Boolean, - onSettingToggled: (AppSetting, Boolean) -> Unit, - onDialogStateChange: (AppSetting?) -> Unit, -) { - // Show warning if user wants to disable the switch - if (!enabled) { - onDialogStateChange(element) - } else { - onSettingToggled(element, true) + is Item.Button -> SettingsButtonItem( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), + item = item, + ) + is Item.Switch -> SettingsSwitchItem( + modifier = Modifier.padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + item = item, + ) + } + } } } // region Preview -@Composable -private fun AppSettingsScreenSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = false, - isTogglesEnabled = true, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Light() { +private fun AppSettingsScreenPreview_Light( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Dark() { +private fun AppSettingsScreenPreview_Dark( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme(isDark = true) { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) { - Column(modifier = modifier.background(TangemTheme.colors.background.primary)) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = true, - isTogglesEnabled = false, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, +private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + val items = persistentListOf( + itemsFactory.createEnrollBiometricsCard {}, + itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {}, + itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), + itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, + itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> }, + itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}), ) - } -} -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() { - TangemTheme { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() { - TangemTheme(isDark = true) { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} + AppSettingsScreenState.Content( + items = items, + dialog = null, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index fbc4640e8f..108b30fec8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -1,11 +1,70 @@ package com.tangem.tap.features.details.ui.appsettings -import com.tangem.tap.features.details.redux.AppSetting +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList -data class AppSettingsScreenState( - val settings: Map = emptyMap(), - val showEnrollBiometricsCard: Boolean = false, - val isTogglesEnabled: Boolean = false, - val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ }, - val onEnrollBiometrics: () -> Unit = { /* no-op */ }, -) \ No newline at end of file +@Immutable +internal sealed class AppSettingsScreenState { + + object Loading : AppSettingsScreenState() + + data class Content( + val items: ImmutableList, + val dialog: Dialog?, + ) : AppSettingsScreenState() + + @Immutable + sealed class Item { + + abstract val id: String + + data class Card( + override val id: String, + @DrawableRes val iconResId: Int, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + ) : Item() + + data class Switch( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val isChecked: Boolean, + val onCheckedChange: (Boolean) -> Unit, + ) : Item() + + data class Button( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val onClick: () -> Unit, + ) : Item() + } + + @Immutable + sealed class Dialog { + + abstract val onDismiss: () -> Unit + + data class Alert( + val title: TextReference, + val description: TextReference, + val confirmText: TextReference, + val onConfirm: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + + data class Selector( + val title: TextReference, + val selectedItemIndex: Int, + val items: ImmutableList, + val onSelect: (Int) -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 857ce03b4d..b7a081674b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,53 +1,200 @@ package com.tangem.tap.features.details.ui.appsettings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.LifecycleCoroutineScope +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.redux.AppSettingsState import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.Store -class AppSettingsViewModel(private val store: Store) { +internal class AppSettingsViewModel( + private val store: Store, + private val detailsFeatureToggles: DetailsFeatureToggles, + private val appCurrencyRepository: AppCurrencyRepository, +) { - fun updateState(state: DetailsState): AppSettingsScreenState { - return with(state.appSettingsState) { - AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to saveWallets, - AppSetting.SaveAccessCode to saveAccessCodes, + private val itemsFactory = AppSettingsItemsFactory() + private val dialogsFactory = AppSettingsDialogsFactory() + + private val appCurrencyUpdatesJobHolder = JobHolder() + + var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading) + private set + + init { + if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) { + bootstrapAppCurrencyUpdates() + } + } + + fun updateState(state: DetailsState) { + uiState = AppSettingsScreenState.Content( + items = buildItems(state.appSettingsState), + dialog = (uiState as? AppSettingsScreenState.Content)?.dialog, + ) + } + + fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { + store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(lifecycleScope)) + } + + private fun buildItems(state: AppSettingsState): ImmutableList { + val items = buildList { + if (state.needEnrollBiometrics) { + itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add) + } + + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = state.selectedFiatCurrency.name, + onClick = ::showAppCurrencySelector, + ).let(::add) + + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } + + itemsFactory.createFlipToHideBalanceSwitch( + isChecked = state.isHidingEnabled, + isEnabled = true, + onCheckedChange = ::onFlipToHideBalanceToggled, + ).let(::add) + + if (state.darkThemeSwitchEnabled) { + itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) { + showThemeModeSelector(state.selectedThemeMode) + }.let(::add) + } + } + + return items.toImmutableList() + } + + private fun enrollBiometrics() { + store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) + } + + private fun showAppCurrencySelector() { + val action = if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) { + NavigationAction.NavigateTo(AppScreen.AppCurrencySelector) + } else { + WalletAction.AppCurrencyAction.ChooseAppCurrency + } + + store.dispatchOnMain(action) + } + + private fun showThemeModeSelector(selectedMode: AppThemeMode) { + updateContentState { + copy( + dialog = dialogsFactory.createThemeModeSelectorDialog( + selectedModeIndex = selectedMode.ordinal, + onSelect = { mode -> + store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode)) + dismissDialog() + }, + onDismiss = ::dismissDialog, ), - showEnrollBiometricsCard = needEnrollBiometrics, - isTogglesEnabled = !needEnrollBiometrics && !isInProgress, - onSettingToggled = { privacySetting, enabled -> - onSettingsToggled(privacySetting, enabled) - }, - onEnrollBiometrics = { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) - }, ) } } + private fun onSaveWalletsToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveWallets, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDeleteSavedWalletsAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveWallets, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onSaveAccessCodesToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveAccessCode, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDeleteSavedAccessCodesAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) } - fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { - store.dispatch( - DetailsAction.AppSettings.CheckBiometricsStatus( - awaitStatusChange = false, - lifecycleCoroutineScope = lifecycleScope, - ), - ) + private fun onFlipToHideBalanceToggled(enable: Boolean) { + store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable)) } - fun refreshBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { - store.dispatch( - DetailsAction.AppSettings.CheckBiometricsStatus( - awaitStatusChange = true, - lifecycleCoroutineScope = lifecycleScope, - ), - ) + private fun dismissDialog() { + updateContentState { copy(dialog = null) } + } + + private fun bootstrapAppCurrencyUpdates() { + appCurrencyRepository + .getSelectedAppCurrency() + .onEach { + if (it.code == store.state.globalState.appCurrency.code) return@onEach + + val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) } + store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency)) + } + .launchIn(scope) + .saveIn(appCurrencyUpdatesJobHolder) + } + + private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { + uiState = when (val state = uiState) { + is AppSettingsScreenState.Content -> block(state) + is AppSettingsScreenState.Loading -> state + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index c82113133b..1dce42e622 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -1,97 +1,60 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.WarningTextButton +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @Composable -internal fun SettingsAlertDialog( - element: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, - onSettingToggle: () -> Unit, -) { - val text = when (element) { - AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message - AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message - } - - AlertDialog( - onDismissRequest = { onDialogStateChange(null) }, - confirmButton = { - TextButton( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = stringResource(id = R.string.common_cancel), - onClick = { - onDialogStateChange(null) - }, - ) - }, - dismissButton = { - WarningTextButton( - text = stringResource(id = R.string.common_delete), - onClick = { - onDialogStateChange(null) - onSettingToggle() - }, - ) - }, - title = { - Text( - text = stringResource(id = R.string.common_attention), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - }, - text = { - Text( - text = stringResource(id = text), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) - }, - shape = TangemTheme.shapes.roundedCornersLarge, +internal fun SettingsAlertDialog(dialog: Dialog.Alert) { + BasicDialog( + title = dialog.title.resolveReference(), + message = dialog.description.resolveReference(), + isDismissable = false, + confirmButton = DialogButton( + title = dialog.confirmText.resolveReference(), + warning = true, + onClick = dialog.onConfirm, + ), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = dialog.onDismiss, + ), + onDismissDialog = dialog.onDismiss, ) } // region Preview -@Composable -private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - SettingsAlertDialog( - element = AppSetting.SaveAccessCode, - onDialogStateChange = {}, - onSettingToggle = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Light() { +private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme { - SettingsAlertDialogSample() + SettingsAlertDialog(dialog = dialog) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Dark() { +private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme(isDark = true) { - SettingsAlertDialogSample() + SettingsAlertDialog(dialog = dialog) } } + +private class AlertDialogProvider : CollectionPreviewParameterProvider( + collection = buildList { + val dialogsFactory = AppSettingsDialogsFactory() + + dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add) + dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt new file mode 100644 index 0000000000..946c62d2f2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.background.secondary, + onClick = item.onClick, + ) { + Column( + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing8, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + horizontalAlignment = Alignment.Start, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme { + SettingsButtonItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme(isDark = true) { + SettingsButtonItem(item = item) + } +} + +private class ButtonItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = "US Dollar", + onClick = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt similarity index 56% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt index 1386a364d7..1560b14a43 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt @@ -1,11 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Surface @@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW16 +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.wallet.R +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun EnrollBiometricsCard(onClick: () -> Unit) { +internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) { Surface( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - color = TangemTheme.colors.background.primary, + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.button.disabled, shape = TangemTheme.shapes.roundedCornersLarge, - onClick = onClick, + onClick = item.onClick, ) { Row( modifier = Modifier.padding(all = 16.dp), @@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { horizontalArrangement = Arrangement.SpaceEvenly, ) { Icon( - painter = painterResource(id = R.drawable.ic_alert_circle_24), + painter = painterResource(id = item.iconResId), tint = TangemTheme.colors.icon.attention, contentDescription = null, ) SpacerW16() Column { Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_title), + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) SpacerH4() Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_description), + text = item.description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) @@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { } // region Preview -@Composable -private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier.background(TangemTheme.colors.background.secondary), - ) { - EnrollBiometricsCard(onClick = {}) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Light() { +private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme { - EnrollBiometricsCardSample() + SettingsCardItem(item = item) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Dark() { +private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme(isDark = true) { - EnrollBiometricsCardSample() + SettingsCardItem(item = item) } } + +private class CardItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createEnrollBiometricsCard( + onClick = { /* no-op */ }, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt new file mode 100644 index 0000000000..8c1a93e307 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.SelectorDialog +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { + SelectorDialog( + title = dialog.title.resolveReference(), + selectedItemIndex = dialog.selectedItemIndex, + items = dialog.items.map { it.resolveReference() }.toImmutableList(), + confirmButton = DialogButton( + title = stringResource(R.string.common_cancel), + onClick = dialog.onDismiss, + ), + onSelect = dialog.onSelect, + onDismissDialog = dialog.onDismiss, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = false) { + SettingsSelectorDialog(param) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = true) { + SettingsSelectorDialog(param) + } +} + +private class DialogProvider : CollectionPreviewParameterProvider( + collection = listOf( + AppSettingsDialogsFactory().createThemeModeSelectorDialog( + selectedModeIndex = 0, + onSelect = {}, + onDismiss = {}, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt new file mode 100644 index 0000000000..78084743b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -0,0 +1,113 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.common.TangemSwitch + +@Composable +internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { + val titleTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + val descriptionTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.tertiary + }, + ) + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier.weight(weight = .9f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = titleTextColor, + ) + SpacerH4() + Text( + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = descriptionTextColor, + ) + } + SpacerW32() + TangemSwitch( + checked = item.isChecked, + enabled = item.isEnabled, + onCheckedChange = item.onCheckedChange, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme { + SettingsSwitchItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme(isDark = true) { + SettingsSwitchItem(item = item) + } +} + +private class SwitchItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 90c489ab41..82431ec211 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -1,69 +1,36 @@ package com.tangem.tap.features.details.ui.cardsettings -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.fragment.app.viewModels import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store -import org.rekotlin.StoreSubscriber +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject -class CardSettingsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class CardSettingsFragment : ComposeFragment() { - private val viewModel = CardSettingsViewModel(store) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder - private var screenState: MutableState = - mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) + private val viewModel: CardSettingsViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + @Composable + override fun ScreenContent(modifier: Modifier) { + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - CardSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } - } - } - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - screenState.value = viewModel.updateState(state.cardSettingsState) + CardSettingsScreen( + modifier = modifier, + state = viewModel.screenState.value, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 29906fd0e0..759e966ed5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -1,102 +1,109 @@ package com.tangem.tap.features.details.ui.cardsettings -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) { +internal fun CardSettingsScreen( + state: CardSettingsScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val needReadCard = state.cardDetails == null SettingsScreensScaffold( + modifier = modifier, content = { if (needReadCard) { - CardSettingsReadCard(state.onScanCardClick) + CardSettingsReadCard(state.onScanCardClick, state.cardImage) } else { CardSettings(state = state) } }, titleRes = R.string.card_settings_title, - backgroundColor = TangemTheme.colors.background.secondary, onBackClick = onBackClick, ) } @Suppress("MagicNumber") @Composable -fun CardSettingsReadCard(onScanCardClick: () -> Unit) { +private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwork?) { Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens.size70), ) { Box( modifier = Modifier .fillMaxWidth() - .padding(bottom = 40.dp), + .padding(start = TangemTheme.dimens.size16, end = TangemTheme.dimens.size16), ) { - Image( + val circleColor = TangemTheme.colors.stroke.primary + Canvas( modifier = Modifier - .fillMaxWidth() - .padding(start = 80.dp, end = 80.dp, top = 70.dp) - .rotate(-15f), - painter = painterResource(id = R.drawable.card_placeholder_secondary), - contentDescription = "", - contentScale = ContentScale.FillWidth, - ) - Image( + .size(300.dp) + .align(Alignment.Center), + ) { + drawCircle( + color = circleColor, + radius = size.minDimension / 2.0f, + ) + } + SubcomposeAsyncImage( + model = ImageRequest.Builder(context = LocalContext.current) + .data(cardArtwork?.artworkId) + .crossfade(enable = true) + .build(), modifier = Modifier - .fillMaxWidth() - .padding(start = 60.dp, end = 60.dp) - .rotate(-1f), - painter = painterResource(id = R.drawable.card_placeholder_black), - contentDescription = "", - contentScale = ContentScale.FillWidth, + .align(Alignment.Center) + .fillMaxWidth(), + loading = { /* no-op */ }, + error = { /* no-op */ }, + contentDescription = null, ) } Spacer(modifier = Modifier.weight(1f)) Column( modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 32.dp), + .padding( + start = TangemTheme.dimens.size16, + end = TangemTheme.dimens.size16, + bottom = TangemTheme.dimens.size32, + ), ) { Text( text = stringResource(id = R.string.scan_card_settings_title), - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h3, ) - Spacer(modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.size(TangemTheme.dimens.size20)) Text( text = stringResource(id = R.string.scan_card_settings_message), - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body1, modifier = Modifier .verticalScroll(rememberScrollState()) .weight(weight = 1f, fill = false), ) - Spacer(modifier = Modifier.size(29.dp)) + Spacer(modifier = Modifier.size(TangemTheme.dimens.size28)) DetailsMainButton( title = stringResource(id = R.string.scan_card_settings_button), onClick = onScanCardClick, @@ -107,7 +114,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) { @Suppress("ComplexMethod") @Composable -fun CardSettings(state: CardSettingsScreenState) { +private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return LazyColumn( @@ -166,8 +173,25 @@ fun CardSettings(state: CardSettingsScreenState) { } } +// region Preview @Composable -@Preview -private fun CardSettingsPreview() { - CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {}) -} \ No newline at end of file +private fun CardSettingsScreenStateSample() { + CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}, onElementClick = {}), {}) +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Light() { + TangemTheme { + CardSettingsScreenStateSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Dark() { + TangemTheme(isDark = true) { + CardSettingsScreenStateSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index e09b4e7c2b..1cf79ec5e4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.features.details.redux.AccessCodeRecoveryState import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.securitymode.toTitleRes @@ -11,14 +12,15 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import com.tangem.wallet.R import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo -data class CardSettingsScreenState( +internal data class CardSettingsScreenState( val cardDetails: List? = null, val accessCodeRecoveryState: AccessCodeRecoveryState? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, + val cardImage: Artwork? = null, ) -sealed class CardInfo( +internal sealed class CardInfo( val titleRes: TextReference, val subtitle: TextReference, val clickable: Boolean = false, @@ -68,7 +70,7 @@ sealed class CardInfo( } // TODO("Remove and use the same from coreUI") -sealed interface TextReference { +internal sealed interface TextReference { class Res(@StringRes val id: Int, val formatArgs: List = emptyList()) : TextReference { constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) } @@ -78,7 +80,7 @@ sealed interface TextReference { @Composable @ReadOnlyComposable -fun TextReference.resolveReference(): String { +internal fun TextReference.resolveReference(): String { return when (this) { is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) is TextReference.Str -> this.value diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 805f373605..732f7aa06d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -1,18 +1,64 @@ package com.tangem.tap.features.details.ui.cardsettings +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import arrow.core.Either import com.tangem.core.analytics.Analytics import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.getTwinCardIdForUser +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.redux.CardSettingsState import com.tangem.tap.features.details.redux.DetailsAction -import org.rekotlin.Store +import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel +import org.rekotlin.StoreSubscriber +import timber.log.Timber +import javax.inject.Inject -class CardSettingsViewModel(private val store: Store) { +@HiltViewModel +internal class CardSettingsViewModel @Inject constructor( + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, +) : + ViewModel(), DefaultLifecycleObserver, StoreSubscriber { - fun updateState(state: CardSettingsState?): CardSettingsScreenState { + var screenState: MutableState = + mutableStateOf(updateState(store.state.detailsState.cardSettingsState)) + + override fun onStart(owner: LifecycleOwner) { + when (val selectedWalletEither = getSelectedWalletSyncUseCase()) { + is Either.Left -> { + Timber.e(selectedWalletEither.value.toString()) + } + is Either.Right -> { + store.dispatchOnMain(WalletAction.UpdateUserWalletArtwork(selectedWalletEither.value.walletId)) + } + } + + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState && + oldState.walletState == newState.walletState + }.select { it.detailsState } + } + } + + override fun onStop(owner: LifecycleOwner) { + store.unsubscribe(this) + } + + override fun newState(state: DetailsState) { + screenState.value = updateState(state.cardSettingsState) + } + + private fun updateState(state: CardSettingsState?): CardSettingsScreenState { return if (state?.manageSecurityState == null) { CardSettingsScreenState( cardDetails = null, @@ -21,6 +67,7 @@ class CardSettingsViewModel(private val store: Store) { onScanCardClick = { store.dispatch(DetailsAction.ScanCard) }, + cardImage = store.state.walletState.cardImage, ) } else { val cardId = if (state.card.isTangemTwins) { @@ -58,6 +105,7 @@ class CardSettingsViewModel(private val store: Store) { onElementClick = { handleClickingItem(it) }, + cardImage = store.state.walletState.cardImage, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index c92047d0bc..35b36c0f00 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -1,33 +1,41 @@ package com.tangem.tap.features.details.ui.common import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @Composable -fun SettingsScreensScaffold( - modifier: Modifier = Modifier, - content: @Composable () -> Unit, - background: @Composable (() -> Unit)? = null, - fab: @Composable (() -> Unit)? = null, - backgroundColor: Color = TangemTheme.colors.background.secondary, - titleRes: Int? = null, +internal fun SettingsScreensScaffold( onBackClick: () -> Unit, + content: @Composable () -> Unit, + modifier: Modifier = Modifier, + @StringRes titleRes: Int? = null, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + fab: @Composable () -> Unit = {}, ) { - BackHandler(true, onBackClick) + val state = rememberScaffoldState(snackbarHostState = snackbarHostState) + val backgroundColor = TangemTheme.colors.background.secondary + + BackHandler(onBack = onBackClick) + SystemBarsEffect { + setSystemBarsColor(backgroundColor) + } Scaffold( + scaffoldState = state, topBar = { EmptyTopBarWithNavigation( onBackClick = onBackClick, @@ -36,34 +44,32 @@ fun SettingsScreensScaffold( }, modifier = modifier.systemBarsPadding(), backgroundColor = backgroundColor, - floatingActionButton = { fab?.invoke() }, - ) { - if (titleRes != null) { - Box(modifier = modifier.fillMaxSize()) { - background?.invoke() - - Column(modifier = modifier.fillMaxWidth()) { + floatingActionButton = fab, + content = { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize(), + ) { + if (titleRes != null) { Text( text = stringResource(id = titleRes), - modifier = modifier.padding( - start = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing20, - bottom = TangemTheme.dimens.spacing54, - ), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing20) + .padding(bottom = TangemTheme.dimens.spacing36), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) - content() } + + content() } - } else { - content() - } - } + }, + ) } @Composable -fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { +internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { Text( text = stringResource(id = titleRes), modifier = modifier.padding(start = 20.dp, end = 20.dp), @@ -73,7 +79,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { } @Composable -fun EmptyTopBarWithNavigation( +internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, backgroundColor: Color = TangemTheme.colors.background.primary, ) { @@ -95,7 +101,12 @@ fun EmptyTopBarWithNavigation( } @Composable -fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +internal fun DetailsMainButton( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { PrimaryButtonIconEnd( text = title, enabled = enabled, @@ -107,7 +118,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M } @Composable -fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { +internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -122,8 +133,8 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean onClick = null, modifier = Modifier.padding(end = 20.dp), colors = RadioButtonDefaults.colors( - unselectedColor = colorResource(id = R.color.icon_secondary), - selectedColor = colorResource(id = R.color.icon_accent), + unselectedColor = TangemTheme.colors.icon.secondary, + selectedColor = TangemTheme.colors.icon.accent, ), ) @@ -131,13 +142,13 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean Text( text = title, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) Spacer(modifier = Modifier.size(4.dp)) Text( text = subtitle, style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt index c6c8e82cf4..9839280049 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt @@ -1,22 +1,12 @@ package com.tangem.tap.features.details.ui.common import androidx.compose.animation.animateColor -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable @@ -25,17 +15,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.tangem.wallet.R +import com.tangem.core.ui.res.TangemTheme @Suppress("MagicNumber") @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = colorResource(id = R.color.control_checked), - uncheckedColor: Color = colorResource(id = R.color.icon_informative), + checkedColor: Color = TangemTheme.colors.icon.accent, + uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, enabled: Boolean = true, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 371b571cdd..d3d215551c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -1,45 +1,48 @@ package com.tangem.tap.features.details.ui.details import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.features.details.DarkThemeFeatureToggle import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store -import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class DetailsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class DetailsFragment : ComposeFragment(), StoreSubscriber { - private val detailsViewModel = DetailsViewModel(store) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Inject + lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle + + @Inject + lateinit var walletsRepository: WalletsRepository + + private lateinit var detailsViewModel: DetailsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + detailsViewModel = DetailsViewModel(store, darkThemeFeatureToggle, walletsRepository) Analytics.send(Settings.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - DetailsScreen( - state = detailsViewModel.detailsScreenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + DetailsScreen( + modifier = modifier, + state = detailsViewModel.detailsScreenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index f05682f878..8a65d0e7bd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -1,156 +1,133 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SystemBarsEffect +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.toImmutableList @Composable -fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { - SystemBarsEffect { - setSystemBarsColor(color = TangemColorPalette.Light1) - } +internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } SettingsScreensScaffold( + modifier = modifier, + snackbarHostState = snackbarHostState, content = { Content(state = state) }, onBackClick = onBackClick, ) + + ShowSnackbarIfNeeded( + snackbarHostState = snackbarHostState, + messageEvent = state.showSnackbar, + ) } @Composable -fun Content(state: DetailsScreenState) { - Box { +private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { + Box(modifier = modifier) { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) - state.elements.map { element -> - if (element == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemsClick = state.onItemsClick) - } else { - DetailsItem( - item = element, - appCurrency = state.appCurrency, - onItemsClick = { state.onItemsClick(element) }, - ) - } - } - Spacer(modifier = Modifier.weight(1f)) - TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", - style = TangemTheme.typography.caption, - color = colorResource(id = R.color.text_tertiary), - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + ScreenTitle(titleRes = R.string.details_title) + SpacerH(height = TangemTheme.dimens.spacing36) + SettingsItems( + items = state.elements, ) + SpacerHMax() + TangemSocialAccounts( + links = state.tangemLinks, + onSocialNetworkClick = state.onSocialNetworkClick, + ) + SpacerH(height = TangemTheme.dimens.spacing12) + TangemAppVersion( + appNameRes = state.appNameRes, + version = state.tangemVersion, + ) + SpacerH(height = TangemTheme.dimens.spacing16) } - ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @Composable -fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { +private fun SettingsItems(items: List) { + items.forEach { item -> + if (item.isLarge) { + LargeDetailsItem(item) + } else { + DetailsItem(item) + } + } +} + +@Composable +private fun LargeDetailsItem(item: SettingsItem) { Row( modifier = Modifier - .defaultMinSize(minHeight = 84.dp) - .fillMaxWidth() - .clickable { onItemsClick(SettingsElement.WalletConnect) }, - horizontalArrangement = Arrangement.Start, + .clickable(onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size84) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = R.drawable.ic_walletconnect), - contentDescription = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.all_colors_azure), - ) + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemColorPalette.Azure, + ) + } Column( - modifier = Modifier.defaultMinSize(minHeight = 56.dp), + modifier = Modifier.heightIn(min = TangemTheme.dimens.size56), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing4, + alignment = Alignment.CenterVertically, + ), ) { Text( - text = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), + text = item.title.resolveReference(), style = TangemTheme.typography.h3, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) - Text( - text = stringResource(id = R.string.wallet_connect_subtitle), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), - style = TangemTheme.typography.body1, - color = colorResource(id = R.color.text_secondary), - ) - } - } -} -@Composable -fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { - Row( - modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .clickable(onClick = onItemsClick), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = item.iconRes), - contentDescription = stringResource(id = item.titleRes), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.icon_secondary), - ) - Column(modifier = Modifier.padding(end = 20.dp)) { - Text( - text = stringResource(id = item.titleRes), - modifier = Modifier, - style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), - ) - if (item == SettingsElement.AppCurrency) { + if (item.subtitle != null) { Text( - text = appCurrency, - style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, ) } } @@ -158,66 +135,159 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> } @Composable -fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { - LazyRow( - modifier = Modifier.padding(start = 8.dp, end = 8.dp), +private fun DetailsItem(item: SettingsItem) { + Row( + modifier = Modifier + .clickable(enabled = !item.showProgress, onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size56) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - items(links) { + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { Icon( - painter = painterResource(id = it.network.iconRes), - contentDescription = "", - modifier = Modifier - .padding(8.dp) - .clickable { onSocialNetworkClick(it) }, - tint = colorResource(id = R.color.icon_informative), + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemTheme.colors.icon.secondary, ) } - } -} -@Composable -fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - val errorTitle = when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled) - EventError.Empty -> "" - } - if (snackbarErrorState != EventError.Empty) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar(errorTitle) - } - when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke() - else -> { - /*no-op*/ - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) } } } } @Composable -@Preview -private fun Preview() { - DetailsScreen( - state = DetailsScreenState( - elements = SettingsElement.values().toList(), +private fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8), + ) { + items(links) { + val onClick = remember(it) { + { onSocialNetworkClick(it) } + } + + IconButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size32), + onClick = onClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = it.network.iconRes), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent) { + var message: TextReference? by remember { mutableStateOf(value = null) } + val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference()) + + LaunchedEffect(resolvedMessage) { + resolvedMessage?.let { + snackbarHostState.showSnackbar(it) + } + } + + EventEffect(messageEvent) { + message = it + } +} + +@Composable +private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + text = "${stringResource(id = appNameRes)} $version", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Light( + @PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState, +) { + TangemTheme(isDark = false) { + DetailsScreen(param, onBackClick = {}) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Dark(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) { + TangemTheme(isDark = true) { + DetailsScreen(param, onBackClick = {}) + } +} + +private class DetailsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = false, {}).let(::add) + SettingsItem.LinkMoreCards({}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.ReferralProgram({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), tangemLinks = TangemSocialAccounts.accountsEn, tangemVersion = "Tangem 2.14.12 (343)", - appCurrency = "Dollar", - onItemsClick = {}, + showSnackbar = consumedEvent(), onSocialNetworkClick = {}, - ), - onBackClick = {}, - ) -} \ No newline at end of file + ).let(::add) + + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = true, {}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), + tangemLinks = TangemSocialAccounts.accountsRu, + tangemVersion = "Tangem 2.14.12 (343)", + showSnackbar = consumedEvent(), + onSocialNetworkClick = {}, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index c0dd629819..6675322128 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -1,82 +1,163 @@ package com.tangem.tap.features.details.ui.details +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable -data class DetailsScreenState( - val elements: List, - val tangemLinks: List, +internal data class DetailsScreenState( + val elements: ImmutableList, + val tangemLinks: ImmutableList, val tangemVersion: String, - val appCurrency: String, - val onItemsClick: (SettingsElement) -> Unit, + val showSnackbar: StateEvent, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, - val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), ) { val appNameRes: Int = R.string.tangem_app_name } @Immutable -enum class SettingsElement( - val iconRes: Int, - val titleRes: Int, +internal sealed class SettingsItem( + @DrawableRes val iconResId: Int, + val title: TextReference, + val subtitle: TextReference? = null, + val isLarge: Boolean = false, ) { - WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), - Chat(R.drawable.ic_chat, R.string.details_chat), - SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), - ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), - CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), - AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency), - AppSettings(R.drawable.ic_settings, R.string.app_settings_title), - LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), - TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App - PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), - TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), + + abstract val onClick: () -> Unit + + open val showProgress: Boolean = false + + data class WalletConnect( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_walletconnect, + title = resourceReference(R.string.wallet_connect_title), + subtitle = resourceReference(R.string.wallet_connect_subtitle), + isLarge = true, + ) + + data class AddWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = resourceReference(R.string.user_wallet_list_add_button), + ) + + data class ScanWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = resourceReference(R.string.scan_card_settings_button), + ) + + data class LinkMoreCards( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_more_cards, + title = resourceReference(R.string.details_row_title_create_backup), + ) + + data class CardSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_card_settings, + title = resourceReference(R.string.card_settings_title), + ) + + data class AppSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_settings, + title = resourceReference(R.string.app_settings_title), + ) + + data class Chat( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_chat, + title = resourceReference(R.string.details_chat), + ) + + data class SendFeedback( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_comment, + title = resourceReference(R.string.details_row_title_send_feedback), + ) + + data class ReferralProgram( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_add_friends, + title = resourceReference(R.string.details_referral_title), + ) + + data class TermsOfService( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_text, + title = resourceReference(R.string.disclaimer_title), + ) + + data class TesterMenu( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_alert_24, + title = resourceReference(R.string.tester_menu), + ) } @Immutable -data class SocialNetworkLink( +internal data class SocialNetworkLink( val network: SocialNetwork, val url: String, ) -sealed class EventError { +internal sealed class EventError { object Empty : EventError() data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError() } sealed class SocialNetwork(val id: String, val iconRes: Int) { - object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter) - object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook) + object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) + object Discord : SocialNetwork("Discord", R.drawable.ic_discord) + object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit) object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram) object GitHub : SocialNetwork("GitHub", R.drawable.ic_github) - object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube) + object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook) object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin) - object Discord : SocialNetwork("Discord", R.drawable.ic_discord) + object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube) } -object TangemSocialAccounts { - val accountsEn: List = listOf( +internal object TangemSocialAccounts { + val accountsEn: ImmutableList = persistentListOf( + SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"), SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), - SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), - SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), + SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"), + SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"), SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"), SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"), - SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"), SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), - SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), + SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"), ) - val accountsRu: List = listOf( + val accountsRu: ImmutableList = persistentListOf( + SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"), SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"), - SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), - SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), + SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"), + SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"), SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"), SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"), - SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"), SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), - SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), + SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"), ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 8301791338..8e9fb4ce18 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -5,111 +5,115 @@ import androidx.compose.runtime.mutableStateOf import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.DarkThemeFeatureToggle +import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.Store -class DetailsViewModel(private val store: Store) { +// TODO: change to Android ViewModel [REDACTED_JIRA] +internal class DetailsViewModel( + private val store: Store, + private val darkThemeFeatureToggle: DarkThemeFeatureToggle, + private val walletsRepository: WalletsRepository, +) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) private set - @Suppress("ComplexMethod") - fun updateState(state: DetailsState): DetailsScreenState { - val cardTypesResolver = state.scanResponse?.cardTypesResolver - val settings = SettingsElement.values().mapNotNull { - when (it) { - SettingsElement.WalletConnect -> { - if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null - } - SettingsElement.SendFeedback -> it - SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null - SettingsElement.PrivacyPolicy -> { - if (state.privacyPolicyUrl != null) it else null - } - SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null - SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null - SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null - else -> it - } - } + init { + bootstrapScreenState() + } + fun updateState(state: DetailsState): DetailsScreenState { return DetailsScreenState( - elements = settings, + elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), - appCurrency = state.appCurrency.name, - onItemsClick = { handleClickingSettingsItem(it) }, - onSocialNetworkClick = { handleSocialNetworkClick(it) }, + showSnackbar = triggerErrorSnackbarIfNeeded(state.error), + onSocialNetworkClick = ::handleSocialNetworkClick, ) } - private fun handleSocialNetworkClick(link: SocialNetworkLink) { - Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatch(NavigationAction.OpenUrl(link.url)) + private fun createSettingsItems(state: DetailsState): ImmutableList { + val scanResponse = state.scanResponse ?: return persistentListOf() + val cardTypesResolver = scanResponse.cardTypesResolver + + return buildList { + SettingsItem.WalletConnect(::navigateToWalletConnect) + .takeIf { cardTypesResolver.isMultiwalletAllowed() } + ?.let(::add) + + SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeIf { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeUnless { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.LinkMoreCards(::linkMoreCards) + .takeIf { state.createBackupAllowed } + ?.let(::add) + + SettingsItem.CardSettings(::navigateToCardSettings) + .let(::add) + + SettingsItem.AppSettings(::navigateToAppSettings) + .let(::add) + + SettingsItem.Chat(::navigateToChat) + .let(::add) + + SettingsItem.SendFeedback(::sendFeedback) + .let(::add) + + SettingsItem.ReferralProgram(::navigateToReferralProgram) + .takeIf { cardTypesResolver.isTangemWallet() } + ?.let(::add) + + SettingsItem.TermsOfService(::navigateToToS) + .let(::add) + + SettingsItem.TesterMenu(::navigateToTesterMenu) + .takeIf { BuildConfig.TESTER_MENU_ENABLED } + ?.let(::add) + }.toImmutableList() } - private fun handleClickingSettingsItem(item: SettingsElement) { - when (item) { - SettingsElement.WalletConnect -> { - Analytics.send(Settings.ButtonWalletConnect()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) - } - SettingsElement.Chat -> { - Analytics.send(Settings.ButtonChat()) - store.dispatch(GlobalAction.OpenChat(SupportInfo())) - } - SettingsElement.SendFeedback -> { - Analytics.send(Settings.ButtonSendFeedback()) - store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) - } - SettingsElement.CardSettings -> { - Analytics.send(Settings.ButtonCardSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) - } - SettingsElement.AppCurrency -> { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } - SettingsElement.AppSettings -> { - Analytics.send(Settings.ButtonAppSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) - } - SettingsElement.LinkMoreCards -> { - Analytics.send(Settings.ButtonCreateBackup()) - store.dispatch(WalletAction.MultiWallet.BackupWallet) - } - SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.Show(AppScreen.Details)) - } - SettingsElement.PrivacyPolicy -> { - // TODO: To be available later - } - SettingsElement.ReferralProgram -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) - } - SettingsElement.TesterMenu -> { - store.state.daggerGraphState.testerRouter?.startTesterScreen() - } - } - } - - private fun getSocialLinks(): List { - val locale = LocaleRegionProvider().getRegion() - return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { - TangemSocialAccounts.accountsRu + private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent { + return if (text == null) { + consumedEvent() } else { - TangemSocialAccounts.accountsEn + triggeredEvent(text) { + store.dispatch(DetailsAction.DismissError) + } } } @@ -118,4 +122,81 @@ class DetailsViewModel(private val store: Store) { val versionName: String = BuildConfig.VERSION_NAME return "$versionName ($versionCode)" } + + private fun navigateToTesterMenu() { + store.state.daggerGraphState.testerRouter?.startTesterScreen() + } + + private fun navigateToToS() { + store.dispatch(DisclaimerAction.Show(AppScreen.Details)) + } + + private fun navigateToReferralProgram() { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + } + + private fun sendFeedback() { + Analytics.send(Settings.ButtonSendFeedback()) + store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } + + private fun navigateToChat() { + Analytics.send(Settings.ButtonChat()) + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + private fun navigateToAppSettings() { + Analytics.send(Settings.ButtonAppSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) + } + + private fun navigateToCardSettings() { + Analytics.send(Settings.ButtonCardSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) + } + + private fun linkMoreCards() { + Analytics.send(Settings.ButtonCreateBackup()) + store.dispatch(WalletAction.MultiWallet.BackupWallet) + } + + private fun scanAndSaveUserWallet() { + Analytics.send(Settings.ScanNewCard) + store.dispatch(DetailsAction.ScanAndSaveUserWallet) + } + + private fun navigateToWalletConnect() { + Analytics.send(Settings.ButtonWalletConnect()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + } + + private fun handleSocialNetworkClick(link: SocialNetworkLink) { + Analytics.send(Settings.ButtonSocialNetwork(link.network)) + store.dispatch(NavigationAction.OpenUrl(link.url)) + } + + private fun getSocialLinks(): ImmutableList { + val locale = LocaleRegionProvider().getRegion() + return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { + TangemSocialAccounts.accountsRu + } else { + TangemSocialAccounts.accountsEn + } + } + + private fun bootstrapScreenState() { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { selectedUserWallet -> + store.dispatchWithMain( + DetailsAction.PrepareScreen( + scanResponse = selectedUserWallet.scanResponse, + darkThemeSwitchEnabled = darkThemeFeatureToggle.isDarkThemeEnabled, + shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(), + ), + ) + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 7a7c723779..9ac4c1f3d3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -1,47 +1,36 @@ package com.tangem.tap.features.details.ui.resetcard -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class ResetCardFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = ResetCardViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - ResetCardScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + ResetCardScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index d64bf444ca..aa76d59a80 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,24 +1,17 @@ package com.tangem.tap.features.details.ui.resetcard -import androidx.compose.foundation.Image +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconToggleButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -34,32 +27,37 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) { +internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( + modifier = modifier, content = { ResetCardView(state = state) }, onBackClick = onBackClick, - backgroundColor = Color.Transparent, ) } +@OptIn(ExperimentalAnimationApi::class) @Suppress("LongMethod", "MagicNumber") @Composable -fun ResetCardView(state: ResetCardScreenState) { +private fun ResetCardView(state: ResetCardScreenState) { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, ) { - Box { - Image( - painter = painterResource(id = R.drawable.ill_reset_background), - contentDescription = null, - modifier = Modifier.offset(y = (-82).dp), + ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 21.dp), + contentAlignment = Alignment.CenterStart, + ) { + Icon( + painter = painterResource(id = R.drawable.img_alert), + contentDescription = "", + tint = Color.Unspecified, ) - ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) } - Spacer(modifier = Modifier.weight(1f)) Column( modifier = Modifier.offset(y = (-32).dp), verticalArrangement = Arrangement.Bottom, @@ -81,54 +79,78 @@ fun ResetCardView(state: ResetCardScreenState) { ) Spacer(modifier = Modifier.size(28.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - onClick = { state.onAcceptWarningToggleClick(!state.accepted) }, - ) - .padding(top = 16.dp, bottom = 16.dp), - ) { - IconToggleButton( - checked = state.accepted, - onCheckedChange = state.onAcceptWarningToggleClick, - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - ) { - Icon( - painter = painterResource( - if (state.accepted) { - R.drawable.ic_accepted - } else { - R.drawable.ic_unticked - }, - ), - contentDescription = null, - tint = if (state.accepted) { - TangemTheme.colors.icon.accent - } else { - TangemTheme.colors.icon.secondary - }, - ) - } - Text( - text = stringResource(id = R.string.reset_card_to_factory_warning_message), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - modifier = Modifier.padding(end = 20.dp), - ) - } + + ConditionCheckBox( + checkedState = state.acceptCondition1Checked, + onCheckedChange = state.onAcceptCondition1ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_1), + ) + + ConditionCheckBox( + checkedState = state.acceptCondition2Checked, + onCheckedChange = state.onAcceptCondition2ToggleClick, + description = TextReference.Res(R.string.reset_card_to_factory_condition_2), + ) Spacer(modifier = Modifier.size(16.dp)) Box( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 32.dp), ) { - DetailsMainButton( - title = stringResource(id = R.string.reset_card_to_factory_button_title), - onClick = state.onResetButtonClick, - enabled = state.resetButtonEnabled, + AnimatedContent( + targetState = state.resetButtonEnabled, + label = "Update checked state", + ) { buttonEnabled -> + DetailsMainButton( + title = stringResource(id = R.string.reset_card_to_factory_button_title), + onClick = state.onResetButtonClick, + enabled = buttonEnabled, + ) + } + } + } + } +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) -> Unit, description: TextReference) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + onClick = { onCheckedChange.invoke(!checkedState) }, + ) + .padding(top = TangemTheme.dimens.size16, bottom = TangemTheme.dimens.size16), + ) { + IconToggleButton( + checked = checkedState, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(start = TangemTheme.dimens.size20, end = TangemTheme.dimens.size20), + ) { + AnimatedContent(targetState = checkedState, label = "Update checked state") { checked -> + Icon( + painter = painterResource( + if (checked) { + R.drawable.ic_accepted + } else { + R.drawable.ic_unticked + }, + ), + contentDescription = null, + tint = if (checked) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.secondary + }, ) } } + Text( + text = description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier.padding(end = TangemTheme.dimens.size20), + ) } } @@ -143,7 +165,8 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { state = ResetCardScreenState( accepted = true, descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message), - onAcceptWarningToggleClick = {}, + onAcceptCondition1ToggleClick = {}, + onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, ), onBackClick = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index fee3e41a5b..97f7a9776e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -2,10 +2,13 @@ package com.tangem.tap.features.details.ui.resetcard import com.tangem.tap.features.details.ui.cardsettings.TextReference -data class ResetCardScreenState( +internal data class ResetCardScreenState( val accepted: Boolean = false, val descriptionText: TextReference, - val onAcceptWarningToggleClick: (Boolean) -> Unit, + val acceptCondition1Checked: Boolean = false, + val acceptCondition2Checked: Boolean = false, + val onAcceptCondition1ToggleClick: (Boolean) -> Unit, + val onAcceptCondition2ToggleClick: (Boolean) -> Unit, val onResetButtonClick: () -> Unit, ) { 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 9ba899bc7d..00d9f515e2 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 @@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import org.rekotlin.Store -class ResetCardViewModel(private val store: Store) { +internal class ResetCardViewModel(private val store: Store) { fun updateState(state: CardSettingsState?): ResetCardScreenState { val descriptionText = state?.cardInfo @@ -15,9 +15,12 @@ class ResetCardViewModel(private val store: Store) { ?: TextReference.Str(value = "") return ResetCardScreenState( - accepted = state?.resetConfirmed ?: false, + accepted = state?.resetButtonEnabled ?: false, descriptionText = descriptionText, - onAcceptWarningToggleClick = { store.dispatch(DetailsAction.ResetToFactory.Confirm(it)) }, + acceptCondition1Checked = state?.condition1Checked ?: false, + acceptCondition2Checked = state?.condition2Checked ?: false, + onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) }, + onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) }, onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index c3b2015297..2767f56545 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -1,47 +1,36 @@ package com.tangem.tap.features.details.ui.securitymode -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.ui.Modifier import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class SecurityModeFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = SecurityModeViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - SecurityModeScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + SecurityModeScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt index ba9c01e84d..fe5164bf2f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -1,10 +1,6 @@ package com.tangem.tap.features.details.ui.securitymode -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) { +internal fun SecurityModeScreen( + state: SecurityModeScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { SettingsScreensScaffold( + modifier = modifier, content = { SecurityModeOptions(state = state) }, // titleRes = R.string.card_settings_security_mode, onBackClick = onBackClick, @@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) } @Composable -fun SecurityModeOptions(state: SecurityModeScreenState) { +private fun SecurityModeOptions(state: SecurityModeScreenState) { Column( modifier = Modifier .fillMaxSize() @@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) { } @Composable -fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { +private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { val selected = option == state.selectedSecurityMode val title = option.toTitleRes() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt index 97482c7c01..27c7629382 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.wallet.R -data class SecurityModeScreenState( +internal data class SecurityModeScreenState( val availableOptions: List, val selectedSecurityMode: SecurityOption, val isSaveChangesEnabled: Boolean, @@ -11,7 +11,7 @@ data class SecurityModeScreenState( val onSaveChangesClicked: () -> Unit, ) -fun SecurityOption.toTitleRes(): Int { +internal fun SecurityOption.toTitleRes(): Int { return when (this) { SecurityOption.LongTap -> R.string.details_manage_security_long_tap SecurityOption.PassCode -> R.string.details_manage_security_passcode diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt index a0666ff19d..7aa863a7df 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Store -class SecurityModeViewModel(val store: Store) { +internal class SecurityModeViewModel(val store: Store) { fun updateState(state: ManageSecurityState?): SecurityModeScreenState { if (state == null) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 2c7ebdea6e..6100201d98 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -1,24 +1,28 @@ package com.tangem.tap.features.details.ui.walletconnect import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater +import androidx.compose.ui.Modifier import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@AndroidEntryPoint +internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -class WalletConnectFragment : Fragment(), StoreSubscriber { private val viewModel = WalletConnectViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.walletConnectState)) @@ -26,32 +30,24 @@ class WalletConnectFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Analytics.send(WalletConnect.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - WalletConnectScreen( - state = screenState.value, - onBackClick = { - if (screenState.value.isLoading) { - store.dispatch( - WalletConnectAction.FailureEstablishingSession( - store.state.walletConnectState.newSessionData?.session?.session, - ), - ) - } - store.dispatch(NavigationAction.PopBackTo()) - }, + @Composable + override fun ScreenContent(modifier: Modifier) { + WalletConnectScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + if (screenState.value.isLoading) { + store.dispatch( + WalletConnectAction.FailureEstablishingSession( + store.state.walletConnectState.newSessionData?.session?.session, + ), ) } - } - } + store.dispatch(NavigationAction.PopBackTo()) + }, + ) } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index c9f4292991..79e8b38b23 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -26,10 +25,15 @@ import com.tangem.wallet.R import kotlinx.collections.immutable.persistentListOf @Composable -fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) { +internal fun WalletConnectScreen( + state: WalletConnectScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val context = LocalContext.current SettingsScreensScaffold( + modifier = modifier, content = { if (state.sessions.isEmpty()) { EmptyScreen(state) @@ -56,8 +60,8 @@ fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) { FloatingActionButton( onClick = onAddSession, - backgroundColor = colorResource(id = R.color.button_primary), - contentColor = colorResource(id = R.color.icon_primary_2), + backgroundColor = TangemTheme.colors.button.primary, + contentColor = TangemTheme.colors.icon.primary2, shape = RoundedCornerShape(16.dp), modifier = modifier, ) { @@ -73,7 +77,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { if (state.isLoading) { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } Column( @@ -86,7 +90,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Image( painter = painterResource(id = R.drawable.ic_walletconnect), contentDescription = "", - colorFilter = ColorFilter.tint(colorResource(id = R.color.icon_inactive)), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), contentScale = ContentScale.FillWidth, modifier = Modifier.width(width = 100.dp), ) @@ -94,7 +98,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Text( text = stringResource(id = R.string.wallet_connect_subtitle), style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_tertiary), + color = TangemTheme.colors.text.tertiary, ) } } @@ -106,7 +110,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { modifier = Modifier .fillMaxWidth() .height(2.dp), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } else { Spacer(modifier = Modifier.height(2.dp)) @@ -127,7 +131,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Text( text = session.description, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, modifier = Modifier.weight(1f), ) IconButton( @@ -139,7 +143,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Icon( painter = painterResource(id = R.drawable.ic_cross_rounded_24), contentDescription = "", - tint = colorResource(id = R.color.icon_warning), + tint = TangemTheme.colors.icon.warning, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt index 24878f3183..d0a711e3f0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import kotlinx.collections.immutable.ImmutableList -data class WalletConnectScreenState( +internal data class WalletConnectScreenState( val sessions: ImmutableList, val isLoading: Boolean = false, val onRemoveSession: (String) -> Unit = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 4707547c51..9713a5da28 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store import timber.log.Timber -class WalletConnectViewModel(private val store: Store) { +internal class WalletConnectViewModel(private val store: Store) { fun updateState(state: WalletConnectState): WalletConnectScreenState { Timber.d("WC2 Sessions: ${state.wc2Sessions}") val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt index aba68641df..d49a684033 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.blockchain.common.Blockchain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -18,7 +19,7 @@ object ApproveWcSessionDialog { sessionBlockchain.fullName, session.peerMeta.url, ) - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_start)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt index 27b3abdb23..cddaab442a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/BnbTransactionDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData @@ -37,7 +38,7 @@ object BnbTransactionDialog { ) val positiveButtonTitle = context.getText(R.string.common_sign) - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(fullMessage) setPositiveButton(positiveButtonTitle) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt index ba70ae5a54..b99df2b02e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ChooseNetworkDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.blockchain.common.Blockchain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -11,7 +12,7 @@ import com.tangem.wallet.R object ChooseNetworkDialog { fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { - return AlertDialog.Builder(context) + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) .setTitle(context.getString(R.string.wallet_connect_select_network)) .setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session)) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index ce79d75c14..301435433b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.redux.global.GlobalAction @@ -11,7 +12,7 @@ import com.tangem.wallet.R object ClipboardOrScanQrDialog { fun create(wcUri: String, context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.common_select_action)) setMessage(context.getText(R.string.wallet_connect_clipboard_alert)) setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt index 2e354e9f0a..4230afdeaf 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/PersonalSignDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -12,7 +13,7 @@ object PersonalSignDialog { fun create(preparedData: WcPreparedRequest.EthSign, context: Context): AlertDialog { val data = preparedData.preparedRequestData.dialogData val message = context.getString(R.string.wallet_connect_alert_sign_message, data.message) - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_sign)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt index ee1ab06996..1dd8aff5a7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/SessionProposalDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.store @@ -21,7 +22,7 @@ object SessionProposalDialog { networks, sessionProposal.url, ) - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(context.getText(R.string.common_start)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt index 21ca09c15c..608eebc130 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/TransactionDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.walletconnect.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -26,7 +27,7 @@ object TransactionDialog { WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign) WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send) } - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.wallet_connect_title)) setMessage(message) setPositiveButton(positiveButtonTitle) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 930f33afe6..e2865c1aee 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundLightGray) + setStatusBarColor(R.color.background_secondary) webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) } store.subscribe(subscriber = this) { state -> @@ -80,10 +80,11 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs exitTransition = inflater.inflateTransition(android.R.transition.slide_top) } AppScreen.Details -> { - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + super.configureTransitions() + } + else -> { + /* no-op */ } - else -> {} } } @@ -122,6 +123,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs webView.loadLocalTermsOfServices() } else -> { + webView.setBackgroundColor(resources.getColor(R.color.transparent, null)) webView.loadUrl(disclaimer.getUri().toString()) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index ceb3ead5f5..22f5ad49d0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -8,6 +8,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat @@ -16,15 +17,16 @@ import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber @AndroidEntryPoint @@ -49,8 +51,9 @@ class HomeFragment : Fragment(), StoreSubscriber { return ComposeView(inflater.context).apply { setContent { TangemTheme { - BackHandler { - requireActivity().finish() + BackHandler(onBack = requireActivity()::finish) + SystemBarsEffect { + setSystemBarsColor(color = Color.Transparent, darkIcons = false) } ScreenContent() } @@ -62,10 +65,10 @@ class HomeFragment : Fragment(), StoreSubscriber { super.onStart() activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) } - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.homeState == newState.homeState - }.select { it.homeState } + store.subscribe(subscriber = this) { state -> + state + .skipRepeats { oldState, newState -> oldState.homeState == newState.homeState } + .select(AppState::homeState) } } @@ -93,11 +96,7 @@ class HomeFragment : Fragment(), StoreSubscriber { onLearn2earnClick = {}, // learn2earnViewModel.uiState.storyScreenState.onClick, onScanButtonClick = { Analytics.send(IntroductionProcess.ButtonScanCard()) - lifecycleScope.launch { - store.dispatch( - HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleScope), - ) - } + store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope)) }, onShopButtonClick = { Analytics.send(IntroductionProcess.ButtonBuyCards()) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt index 76ef1a643e..5265c91db4 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt @@ -96,6 +96,8 @@ fun StoriesTextAnimation( @Composable fun StoriesBottomImageAnimation( initialScale: Float = 2.5f, + secondStageScale: Float = SCALE_SWITCH_BARRIER, + targetScale: Float = 1.0f, firstStepDuration: Int, totalDuration: Int, content: @Composable (Modifier) -> Unit, @@ -117,7 +119,7 @@ fun StoriesBottomImageAnimation( ) }, label = "Appearing scale", - ) { value -> if (value) SCALE_SWITCH_BARRIER else initialScale } + ) { value -> if (value) secondStageScale else initialScale } val secondTransition = updateTransition( targetState = isSecondStepLaunched.value, @@ -131,14 +133,14 @@ fun StoriesBottomImageAnimation( ) }, label = "Outgoing scale", - ) { value -> if (value) 1f else SCALE_SWITCH_BARRIER } + ) { value -> if (value) targetScale else secondStageScale } val fadeIn = firstTransition.animateFloat( transitionSpec = { tween(durationMillis = 400) }, label = "Fade in on start", ) { value -> if (value) 1f else 0f } - if (firstScaleStep.value == SCALE_SWITCH_BARRIER) { + if (firstScaleStep.value == secondStageScale) { isSecondStepLaunched.value = true } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt index cd4af4ff04..456084062b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreen.kt @@ -12,18 +12,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.learn2earn.presentation.ui.Learn2earnStoriesScreen import com.tangem.tap.features.home.compose.content.* @@ -43,7 +38,6 @@ fun StoriesScreen( onShopButtonClick: () -> Unit, onSearchTokensClick: () -> Unit, ) { - val systemUiController = rememberSystemUiController() val state = homeState.value var currentStory by remember { mutableStateOf(state.firstStory) } @@ -62,13 +56,6 @@ fun StoriesScreen( } } - LaunchedEffect(key1 = currentStory.isDarkBackground) { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = !currentStory.isDarkBackground, - ) - } - StoriesScreenContent( modifier = Modifier.fillMaxSize(), config = StoriesScreenContentConfig( @@ -90,13 +77,12 @@ fun StoriesScreen( @Composable private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) { var isPressed by remember { mutableStateOf(value = false) } - var hideContent by remember { mutableStateOf(value = true) } val isPaused = isPressed || config.isScanInProgress val currentStoryDuration = config.currentStory.duration Box( - modifier = modifier.background(Color(0xFF090E13)), + modifier = modifier.background(Color(0xFF010101)), ) { Row( modifier = Modifier.fillMaxSize(), @@ -138,14 +124,6 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M }, ) } - if (!config.currentStory.isDarkBackground) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painterResource(id = R.drawable.ic_overlay), - contentDescription = null, - contentScale = ContentScale.FillBounds, - ) - } Column( modifier = Modifier @@ -166,30 +144,23 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M contentDescription = null, contentScale = ContentScale.FillHeight, modifier = Modifier - .padding(start = 16.dp, top = 10.dp) - .height(17.dp) - .alpha(if (hideContent) 0f else 1f) + .padding( + start = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + .height(TangemTheme.dimens.size18) .align(Alignment.Start), - colorFilter = if (config.currentStory.isDarkBackground) { - null - } else { - ColorFilter.tint(TangemColorPalette.Dark6) - }, ) when (config.currentStory) { Stories.OneInchPromo -> Learn2earnStoriesScreen(config.onLearn2earnClick) Stories.TangemIntro -> FirstStoriesContent( isPaused = isPaused, duration = currentStoryDuration, - isNewWalletAvailable = config.currentStory.isNewWalletAvailable, - ) { - hideContent = it - } - Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet(currentStoryDuration) + ) + Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet() is Stories.UltraSecureBackup -> StoriesUltraSecureBackup( isPaused = isPaused, stepDuration = currentStoryDuration, - isNewWalletAvailable = config.currentStory.isNewWalletAvailable, ) Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration) Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration) @@ -223,7 +194,6 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M ) { HomeButtons( modifier = Modifier.fillMaxWidth(), - isDarkBackground = config.currentStory.isDarkBackground, btnScanStateInProgress = config.isScanInProgress, onScanButtonClick = config.onScanButtonClick, onShopButtonClick = config.onShopButtonClick, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt index 406064b704..198f6930da 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt @@ -5,79 +5,56 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation import com.tangem.tap.features.home.compose.StoriesTextAnimation import com.tangem.wallet.R @Composable -fun StoriesRevolutionaryWallet(stepDuration: Int) { +fun StoriesRevolutionaryWallet() { SplitContent( topContent = { TopContent( titleText = stringResource(id = R.string.story_awe_title), - subtitleText = stringResource(id = R.string.story_awe_description).annotated(), - isDarkBackground = true, + subtitleText = stringResource(id = R.string.story_awe_description), ) }, bottomContent = { - StoriesBottomImageAnimation( - totalDuration = stepDuration, - firstStepDuration = 300, - ) { modifier -> - StoriesImage( - modifier = modifier, - drawableResId = R.drawable.revolutionary_wallet, - isDarkBackground = true, - ) - } + SpacerH32() + StoriesImage( + modifier = Modifier, + drawableResId = R.drawable.img_revolutionary_wallet, + ) }, ) } @Composable -fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int, isNewWalletAvailable: MutableState) { - val subtitleText = buildAnnotatedString { - append(stringResource(id = R.string.story_backup_description_1)) - append(" ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(stringResource(id = R.string.story_backup_description_2_bold)) - } - append(" ") - append(stringResource(id = R.string.story_backup_description_3)) - } +fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) { SplitContent( topContent = { TopContent( titleText = stringResource(id = R.string.story_backup_title), - subtitleText = subtitleText, - isDarkBackground = false, + subtitleText = stringResource(id = R.string.story_backup_description), ) }, bottomContent = { + SpacerH32() FloatingCardsContent( isPaused = isPaused, stepDuration = stepDuration, - isNewWalletAvailable = isNewWalletAvailable, ) }, ) @@ -89,11 +66,11 @@ fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) { topContent = { TopContent( titleText = stringResource(id = R.string.story_currencies_title), - subtitleText = stringResource(id = R.string.story_currencies_description).annotated(), - isDarkBackground = false, + subtitleText = stringResource(id = R.string.story_currencies_description), ) }, bottomContent = { + SpacerH32() StoriesCurrenciesContent(paused = isPaused, duration = stepDuration) }, ) @@ -105,11 +82,11 @@ fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) { topContent = { TopContent( titleText = stringResource(id = R.string.story_web3_title), - subtitleText = stringResource(id = R.string.story_web3_description).annotated(), - isDarkBackground = false, + subtitleText = stringResource(id = R.string.story_web3_description), ) }, bottomContent = { + SpacerH(TangemTheme.dimens.spacing70) StoriesWeb3Content(paused = isPaused, duration = stepDuration) }, ) @@ -121,20 +98,24 @@ fun StoriesWalletForEveryone(stepDuration: Int) { topContent = { TopContent( titleText = stringResource(id = R.string.story_finish_title), - subtitleText = stringResource(id = R.string.story_finish_description).annotated(), - isDarkBackground = true, + subtitleText = stringResource(id = R.string.story_finish_description), ) }, bottomContent = { - StoriesBottomImageAnimation( - totalDuration = stepDuration, - firstStepDuration = 500, - ) { modifier -> - StoriesImage( - modifier = modifier, - drawableResId = R.drawable.wallet_for_everyone, - isDarkBackground = true, - ) + SpacerH32() + BoxWithGradient { + StoriesBottomImageAnimation( + initialScale = 2.6f, + secondStageScale = 1.2f, + targetScale = 1.1f, + totalDuration = stepDuration, + firstStepDuration = 500, + ) { modifier -> + StoriesImage( + modifier = modifier, + drawableResId = R.drawable.img_tangem_for_everyone, + ) + } } }, ) @@ -154,22 +135,20 @@ private fun SplitContent(topContent: @Composable () -> Unit, bottomContent: @Com } @Composable -private fun TopContent(titleText: String, subtitleText: AnnotatedString, isDarkBackground: Boolean) { - SpacerH32() +private fun TopContent(titleText: String, subtitleText: String) { + SpacerH(TangemTheme.dimens.spacing36) StoriesTitleText( text = titleText, - isDarkBackground = isDarkBackground, ) SpacerH16() StoriesSubtitleText( subtitleText = subtitleText, ) - SpacerH32() } @Suppress("MagicNumber") @Composable -private fun StoriesTitleText(text: String, isDarkBackground: Boolean) { +private fun StoriesTitleText(text: String) { StoriesTextAnimation( slideInDuration = 500, slideInDelay = 150, @@ -178,10 +157,8 @@ private fun StoriesTitleText(text: String, isDarkBackground: Boolean) { modifier = modifier .padding(start = 40.dp, end = 40.dp), text = text, - fontSize = 32.sp, - lineHeight = 38.sp, - fontWeight = FontWeight.SemiBold, - color = if (isDarkBackground) Color.White else Color(0xFF090E13), + style = TangemTheme.typography.head, + color = TangemColorPalette.White, textAlign = TextAlign.Center, ) } @@ -189,9 +166,7 @@ private fun StoriesTitleText(text: String, isDarkBackground: Boolean) { @Suppress("MagicNumber") @Composable -private fun StoriesSubtitleText(subtitleText: AnnotatedString) { - val color = Color(0xFFA6AAAD) - +private fun StoriesSubtitleText(subtitleText: String) { StoriesTextAnimation( slideInDuration = 500, slideInDelay = 400, @@ -199,35 +174,28 @@ private fun StoriesSubtitleText(subtitleText: AnnotatedString) { Text( modifier = modifier .padding(start = 40.dp, end = 40.dp), - fontWeight = FontWeight.Normal, text = subtitleText, - fontSize = 20.sp, - lineHeight = 26.sp, - color = color, + style = TangemTheme.typography.body1, + color = TangemColorPalette.Dark1, textAlign = TextAlign.Center, ) } } @Composable -private fun StoriesImage(@DrawableRes drawableResId: Int, isDarkBackground: Boolean, modifier: Modifier = Modifier) { +private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = Modifier) { Image( painter = painterResource(id = drawableResId), contentDescription = null, - contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, - modifier = modifier.fillMaxWidth(), + contentScale = ContentScale.Inside, + modifier = modifier.fillMaxSize(), ) } -private fun String.annotated(): AnnotatedString { - val source = this - return buildAnnotatedString { append(source) } -} - @Preview @Composable private fun RevolutionaryWalletPreview() { - StoriesRevolutionaryWallet(6000) + StoriesRevolutionaryWallet() } @Preview @@ -236,9 +204,6 @@ private fun UltraSecureBackupPreview() { StoriesUltraSecureBackup( false, 6000, - remember { - mutableStateOf(true) - }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt index 7eb1b156ad..ec1cb88957 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt @@ -40,7 +40,7 @@ fun StoriesCurrenciesContent(paused: Boolean, duration: Int) { val decreaseRate = remember { 1f / currencyDrawableList.size } val designItemHeight = remember { 82.dp } - LightenBox { + BoxWithGradient { Column(modifier = Modifier.graphicsLayer(clip = false)) { currencyDrawableList.forEachIndexed { index, drawableResId -> val painter = painterResource(id = drawableResId) @@ -72,7 +72,7 @@ fun StoriesCurrenciesContent(paused: Boolean, duration: Int) { fun StoriesWeb3Content(paused: Boolean, duration: Int) { val dappsItemList = remember { listOf( - R.drawable.dapps0, + R.drawable.dapps1, R.drawable.dapps1, R.drawable.dapps2, R.drawable.dapps3, @@ -84,7 +84,7 @@ fun StoriesWeb3Content(paused: Boolean, duration: Int) { val decreaseRate = remember { 1f / dappsItemList.size } val designItemHeight = 75.dp - LightenBox { + BoxWithGradient { Column(modifier = Modifier.graphicsLayer(clip = false)) { dappsItemList.forEachIndexed { index, drawableResId -> val painter = painterResource(id = drawableResId) @@ -111,7 +111,7 @@ fun StoriesWeb3Content(paused: Boolean, duration: Int) { } @Composable -private fun LightenBox(content: @Composable () -> Unit) { +internal fun BoxWithGradient(content: @Composable () -> Unit) { val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) Box(modifier = Modifier.fillMaxSize()) { @@ -133,9 +133,9 @@ private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize { private val BottomGradient: Brush = Brush.verticalGradient( colors = listOf( - TangemColorPalette.White.copy(alpha = 0f), - TangemColorPalette.White.copy(alpha = 0.75f), - TangemColorPalette.White.copy(alpha = 0.95f), - TangemColorPalette.White, + TangemColorPalette.Black.copy(alpha = 0f), + TangemColorPalette.Black.copy(alpha = 0.75f), + TangemColorPalette.Black.copy(alpha = 0.95f), + TangemColorPalette.Black, ), ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt index 14f51add03..1577eecad8 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt @@ -1,46 +1,36 @@ package com.tangem.tap.features.home.compose.content -import androidx.annotation.StringRes import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.common.compose.FontSizeRange -import com.tangem.tap.common.compose.TextAutoSize -import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation import com.tangem.tap.features.home.compose.StoriesTextAnimation import com.tangem.wallet.R @Suppress("LongMethod", "ComplexMethod", "MagicNumber") @Composable -fun FirstStoriesContent( - isPaused: Boolean, - duration: Int, - isNewWalletAvailable: MutableState, - onHideContent: (Boolean) -> Unit, -) { - val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) - - val screenState = remember { mutableStateOf(StartingScreenState.INIT) } +fun FirstStoriesContent(isPaused: Boolean, duration: Int) { val progress = remember { Animatable(0f) } LaunchedEffect(isPaused) { @@ -57,142 +47,47 @@ fun FirstStoriesContent( } } - when (progress.value) { - in 0f..0.2f -> screenState.value = StartingScreenState.INIT - in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY - in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE - in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND - in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY - in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE - in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW - in 0.8f..1f -> screenState.value = StartingScreenState.LEND - in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD - in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM - } - - if (screenState.value == StartingScreenState.INIT) onHideContent(true) - if (screenState.value == StartingScreenState.BUY) onHideContent(false) - val style = TextStyle( - fontSize = 60.sp, + fontSize = 46.sp, fontWeight = FontWeight.SemiBold, color = Color.White, textAlign = TextAlign.Center, ) - val textId = screenState.textId() - Box(modifier = Modifier.fillMaxSize()) { - if (screenState.isSplashingTextDisplaying()) { - TextAutoSize( - modifier = Modifier - .align(Alignment.Center) - .padding(start = 20.dp, end = 20.dp, bottom = 100.dp), - text = textId?.let { stringResource(textId) } ?: "", - textStyle = style, - fontSizeRange = FontSizeRange(20.sp, 60.sp), + Column( + modifier = Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH(TangemTheme.dimens.spacing94) + StoriesTextAnimation( + slideInDuration = 500, + slideInDelay = 150, + ) { modifier -> + Text( + modifier = modifier, + text = stringResource(R.string.story_meet_title), + style = style, + color = TangemColorPalette.White, + textAlign = TextAlign.Center, ) - } else { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .weight(0.7f), - ) { - if (screenState.isMeetTangemDisplaying()) { - StoriesTextAnimation( - slideInDelay = 0, - ) { modifier -> - TextAutoSize( - modifier = modifier - .padding(start = 20.dp, end = 20.dp, top = 50.dp) - .alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f), - text = textId?.let { stringResource(textId) } ?: "", - textStyle = style, - fontSizeRange = FontSizeRange(30.sp, 50.sp), - ) - } - } - } - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1.2f) - .wrapContentSize(), - ) { - val painter = if (isNewWalletAvailable.value) { - painterResource(id = R.drawable.img_meet_tangem2) - } else { - painterResource(id = R.drawable.img_meet_tangem) - } - StoriesBottomImageAnimation( - totalDuration = duration, - firstStepDuration = 400, - ) { modifier -> - Image( - modifier = modifier.fillMaxWidth(), - painter = painter, - contentDescription = "Tangem Wallet card", - ) - } - } - } } - - Box( + SpacerH(TangemTheme.dimens.spacing46) + Image( modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(TangemTheme.dimens.size72 + bottomInsetsPx.dp) - .background(BottomGradient), + .fillMaxWidth(), + painter = painterResource(R.drawable.img_meet_tangem), + contentScale = ContentScale.Inside, + contentDescription = "Tangem Wallet card", ) } } -private val BottomGradient: Brush = Brush.verticalGradient( - colors = listOf( - TangemColorPalette.Black.copy(alpha = 0f), - TangemColorPalette.Black.copy(alpha = 0.75f), - TangemColorPalette.Black.copy(alpha = 0.95f), - TangemColorPalette.Black, - ), -) - -private enum class StartingScreenState { - INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM -} - -@StringRes -private fun MutableState.textId(): Int? = when (this.value) { - StartingScreenState.INIT -> null - StartingScreenState.BUY -> R.string.story_meet_buy - StartingScreenState.STORE -> R.string.story_meet_store - StartingScreenState.SEND -> R.string.story_meet_send - StartingScreenState.PAY -> R.string.story_meet_pay - StartingScreenState.EXCHANGE -> R.string.story_meet_exchange - StartingScreenState.BORROW -> R.string.story_meet_borrow - StartingScreenState.LEND -> R.string.story_meet_lend - StartingScreenState.SHOW_CARD -> R.string.story_meet_title - StartingScreenState.MEET_TANGEM -> R.string.story_meet_title -} - -private fun MutableState.isSplashingTextDisplaying(): Boolean { - return this.value != StartingScreenState.MEET_TANGEM && - this.value != StartingScreenState.SHOW_CARD -} - -private fun MutableState.isMeetTangemDisplaying(): Boolean { - return this.value == StartingScreenState.MEET_TANGEM -} - @Preview @Composable private fun FirstStoriesPreview() { FirstStoriesContent( false, 8000, - remember { - mutableStateOf(false) - }, - ) {} + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt index 3731a0e13c..6a010d28ed 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt @@ -1,18 +1,11 @@ package com.tangem.tap.features.home.compose.content import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.tap.common.compose.extensions.AnimatedValue import com.tangem.tap.common.compose.extensions.asImageBitmap import com.tangem.tap.common.compose.extensions.toAnimatable @@ -22,13 +15,8 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ @Composable -fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int, isNewWalletAvailable: MutableState) { - val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) - val imageBitmap = if (isNewWalletAvailable.value) { - asImageBitmap(R.drawable.img_card_placeholder_wallet_2) - } else { - asImageBitmap(R.drawable.card_placeholder_wallet) - } +fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int) { + val imageBitmap = asImageBitmap(R.drawable.img_card_placeholder_wallet_2) val cards = listOf( FloatingCard.first(), FloatingCard.second(), @@ -44,14 +32,6 @@ fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int, isNewWalletAvaila stepDuration = stepDuration, ) } - - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(bottomInsetsPx.dp) - .background(BottomGradient), - ) } } @@ -114,12 +94,4 @@ private object FloatingCard { rotationZ = -45f to -30f, scale = 0.6f to 0.75f, ) -} - -private val BottomGradient: Brush = Brush.verticalGradient( - colors = listOf( - TangemColorPalette.White.copy(alpha = 0f), - TangemColorPalette.White.copy(alpha = 0.75f), - TangemColorPalette.White.copy(alpha = 0.95f), - ), -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index e6ae78c415..1bd1a192c6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -24,7 +24,6 @@ import com.tangem.wallet.R @Suppress("MagicNumber") @Composable internal fun HomeButtons( - isDarkBackground: Boolean, btnScanStateInProgress: Boolean, onScanButtonClick: () -> Unit, onShopButtonClick: () -> Unit, @@ -36,31 +35,24 @@ internal fun HomeButtons( ) { ScanCardButton( modifier = Modifier.weight(weight = 1f), - isDarkBackground = isDarkBackground, showProgress = btnScanStateInProgress, onClick = onScanButtonClick, ) SpacerW8() OrderCardButton( modifier = Modifier.weight(weight = 1f), - isDarkBackground = isDarkBackground, onClick = onShopButtonClick, ) } } @Composable -private fun ScanCardButton( - isDarkBackground: Boolean, - showProgress: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { TangemButton( modifier = modifier, text = stringResource(id = R.string.home_button_scan), icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), - colors = if (isDarkBackground) DarkBgScanCardButtonColors else LightBgScanCardButtonColors, + colors = LightBgScanCardButtonColors, showProgress = showProgress, enabled = true, onClick = onClick, @@ -68,12 +60,12 @@ private fun ScanCardButton( } @Composable -private fun OrderCardButton(isDarkBackground: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) { TangemButton( modifier = modifier, text = stringResource(id = R.string.home_button_order), icon = TangemButtonIconPosition.None, - colors = if (isDarkBackground) DarkBgOrderCardButtonColors else LightBgOrderCardButtonColors, + colors = LightBgOrderCardButtonColors, showProgress = false, enabled = true, onClick = onClick, @@ -87,13 +79,6 @@ private val LightBgScanCardButtonColors: ButtonColors = TangemButtonColors( disabledContentColor = TangemColorPalette.Dark6, ) -private val DarkBgScanCardButtonColors: ButtonColors = TangemButtonColors( - backgroundColor = TangemColorPalette.Dark5, - contentColor = TangemColorPalette.White, - disabledBackgroundColor = TangemColorPalette.Dark5, - disabledContentColor = TangemColorPalette.White, -) - private val LightBgOrderCardButtonColors: ButtonColors = TangemButtonColors( backgroundColor = TangemColorPalette.Dark6, contentColor = TangemColorPalette.White, @@ -101,24 +86,16 @@ private val LightBgOrderCardButtonColors: ButtonColors = TangemButtonColors( disabledContentColor = TangemColorPalette.White, ) -private val DarkBgOrderCardButtonColors: ButtonColors = TangemButtonColors( - backgroundColor = TangemColorPalette.Light1, - contentColor = TangemColorPalette.Dark6, - disabledBackgroundColor = TangemColorPalette.Light1, - disabledContentColor = TangemColorPalette.Dark6, -) - // region Preview @Preview(showBackground = true, widthDp = 360) @Composable private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) { TangemTheme { Box( - modifier = Modifier.background(if (state.isDarkBackground) Color.Black else Color.White), + modifier = Modifier.background(Color.Black), ) { HomeButtons( modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - isDarkBackground = state.isDarkBackground, btnScanStateInProgress = state.btnScanStateInProgress, onScanButtonClick = {}, onShopButtonClick = {}, @@ -130,26 +107,15 @@ private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::c private class HomeButtonsParameterProvider : CollectionPreviewParameterProvider( collection = listOf( HomeButtonsState( - isDarkBackground = false, btnScanStateInProgress = false, ), HomeButtonsState( - isDarkBackground = true, - btnScanStateInProgress = false, - ), - HomeButtonsState( - isDarkBackground = false, - btnScanStateInProgress = true, - ), - HomeButtonsState( - isDarkBackground = true, btnScanStateInProgress = true, ), ), ) private data class HomeButtonsState( - val isDarkBackground: Boolean, val btnScanStateInProgress: Boolean, ) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt index c00e0bed32..722dd4f481 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/SearchCurrenciesButton.kt @@ -24,8 +24,8 @@ internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Mo } private val SearchCurrenciesButtonColors: ButtonColors = TangemButtonColors( - backgroundColor = TangemColorPalette.Light2, - contentColor = TangemColorPalette.Dark6, - disabledBackgroundColor = TangemColorPalette.Light2, - disabledContentColor = TangemColorPalette.Dark6, + backgroundColor = TangemColorPalette.Dark5, + contentColor = TangemColorPalette.White, + disabledBackgroundColor = TangemColorPalette.Dark5, + disabledContentColor = TangemColorPalette.White, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt index 2774f0f683..a8a1fd7bc1 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt @@ -1,19 +1,26 @@ package com.tangem.tap.features.home.compose.views +import android.provider.Settings import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerW4 +import com.tangem.core.ui.res.TangemTheme +import kotlinx.coroutines.delay + +private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L @Composable fun StoriesProgressBar( @@ -25,18 +32,30 @@ fun StoriesProgressBar( ) { val progress = remember(currentStep) { Animatable(0f) } - LaunchedEffect(paused, currentStep) { + val context = LocalContext.current + val animatorSpeed = Settings.Global.getFloat( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE, + 1f, + ) + + LaunchedEffect(paused, currentStep, animatorSpeed) { if (paused) { progress.stop() } else { - progress.animateTo( - targetValue = 1f, - animationSpec = tween( - durationMillis = (stepDuration * (1f - progress.value)).toInt(), - easing = LinearEasing, - ), - ) - progress.snapTo(0f) + if (animatorSpeed == 0f) { + progress.snapTo(1f) + delay(STORIES_ANIMATION_SPEED_ZERO_DURATION) + } else { + progress.animateTo( + targetValue = 1f, + animationSpec = tween( + durationMillis = (stepDuration * (1f - progress.value)).toInt(), + easing = LinearEasing, + ), + ) + progress.snapTo(0f) + } onStepFinish() } } @@ -44,18 +63,23 @@ fun StoriesProgressBar( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier -// .height() - .padding(start = 9.dp, end = 9.dp, top = 16.dp), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ), ) { for (index in 0..steps) { Row( modifier = Modifier - .height(2.dp) + .height(TangemTheme.dimens.size2) .weight(1f) + .clip(RoundedCornerShape(TangemTheme.dimens.radius2)) .background(Color.White.copy(alpha = 0.4f)), ) { Box( modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius2)) .background(Color.White) .fillMaxHeight().let { when (index) { diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 033171c4f4..4b83396c14 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.home.redux -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.IndeterminateProgressButton +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class HomeAction : Action { @@ -17,13 +17,12 @@ sealed class HomeAction : Action { /** * Action for scanning card * - * @property analyticsEvent analytics event - * @property lifecycleCoroutineScope lifecycle scope. It will be canceled when lifecycle-aware component is - * destroyed. + * @property analyticsEvent analytics event + * @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed */ data class ReadCard( val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction), - val lifecycleCoroutineScope: LifecycleCoroutineScope, + val scope: CoroutineScope, ) : HomeAction() data class ScanInProgress(val scanInProgress: Boolean) : HomeAction() 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 48f2f37be5..e13c8cff2e 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 @@ -1,6 +1,5 @@ package com.tangem.tap.features.home.redux -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess @@ -63,7 +62,9 @@ private fun handleHomeAction(action: Action) { store.dispatch(GlobalAction.FetchUserCountry) } is HomeAction.ReadCard -> { - readCard(action.analyticsEvent, action.lifecycleCoroutineScope) + action.scope.launch { + readCard(action.analyticsEvent) + } } is HomeAction.GoToShop -> { Analytics.send(Shop.ScreenOpened()) @@ -78,34 +79,31 @@ private fun handleHomeAction(action: Action) { } } -private fun readCard(analyticsEvent: AnalyticsEvent?, lifecycleCoroutineScope: LifecycleCoroutineScope) { - lifecycleCoroutineScope.launch { - delay(timeMillis = 200) - store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, - ) +private suspend fun readCard(analyticsEvent: AnalyticsEvent?) { + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( - analyticsEvent = analyticsEvent, - onProgressStateChange = { showProgress -> - if (showProgress) { - changeButtonState(ButtonState.PROGRESS) - } else { - changeButtonState(ButtonState.ENABLED) - } - }, - onScanStateChange = { scanInProgress -> - store.dispatch(HomeAction.ScanInProgress(scanInProgress)) - }, - onFailure = { - Timber.e(it, "Unable to scan card") + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = analyticsEvent, + onProgressStateChange = { showProgress -> + if (showProgress) { + changeButtonState(ButtonState.PROGRESS) + } else { changeButtonState(ButtonState.ENABLED) - }, - onSuccess = { scanResponse -> - proceedWithScanResponse(scanResponse) - }, - ) - } + } + }, + onScanStateChange = { scanInProgress -> + store.dispatch(HomeAction.ScanInProgress(scanInProgress)) + }, + onFailure = { + Timber.e(it, "Unable to scan card") + changeButtonState(ButtonState.ENABLED) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 3456bafb5d..d2674c5f2a 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -49,15 +49,14 @@ data class HomeState( } sealed class Stories( - val isDarkBackground: Boolean, val duration: Int, val isNewWalletAvailable: MutableState = mutableStateOf(HomeState.isNewWalletAvailableInit()), ) { - object OneInchPromo : Stories(true, duration = 8000) - object TangemIntro : Stories(true, duration = 8000) - object RevolutionaryWallet : Stories(true, duration = 6000) - object UltraSecureBackup : Stories(false, duration = 6000) - object Currencies : Stories(false, duration = 6000) - object Web3 : Stories(false, duration = 6000) - object WalletForEveryone : Stories(true, duration = 6000) + object OneInchPromo : Stories(duration = 8000) + object TangemIntro : Stories(duration = 6000) + object RevolutionaryWallet : Stories(duration = 6000) + object UltraSecureBackup : Stories(duration = 6000) + object Currencies : Stories(duration = 6000) + object Web3 : Stories(duration = 6000) + object WalletForEveryone : Stories(duration = 6000) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt index ba008d4e5a..7458f759ba 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/IntentHandler.kt @@ -6,5 +6,6 @@ import android.content.Intent [REDACTED_AUTHOR] */ interface IntentHandler { - suspend fun handleIntent(intent: Intent?): Boolean + + fun handleIntent(intent: Intent?): Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 09a682cdb5..cd46a39fad 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,21 +4,19 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build -import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.store -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.CoroutineScope /** [REDACTED_AUTHOR] */ class BackgroundScanIntentHandler( private val hasSavedUserWalletsProvider: () -> Boolean, - private val lifecycleCoroutineScope: LifecycleCoroutineScope, + private val scope: CoroutineScope, ) : IntentHandler { private val nfcActions = arrayOf( @@ -27,7 +25,7 @@ class BackgroundScanIntentHandler( NfcAdapter.ACTION_TAG_DISCOVERED, ) - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { if (intent == null || intent.action !in nfcActions) return false val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -40,13 +38,9 @@ class BackgroundScanIntentHandler( intent.action = null if (hasSavedUserWalletsProvider.invoke()) { - // TODO: Remove delay after [REDACTED_JIRA] - lifecycleCoroutineScope.launch { - delay(timeMillis = 200) - store.dispatchWithMain(WelcomeAction.ProceedWithCard(lifecycleCoroutineScope)) - } + store.dispatchOnMain(WelcomeAction.ProceedWithCard) } else { - store.dispatchWithMain(HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleCoroutineScope)) + store.dispatchOnMain(HomeAction.ReadCard(scope = scope)) } return true diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt index 39dda7cd24..e0c509b3ff 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BuyCurrencyIntentHandler.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent import android.net.Uri import com.tangem.core.analytics.Analytics +import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder import com.tangem.tap.store @@ -14,14 +14,14 @@ import com.tangem.tap.store */ class BuyCurrencyIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { val data = intent?.data ?: return false val currency = store.state.walletState.selectedCurrency ?: return false val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL) return if (data.host == successUri.host && data.authority == successUri.authority) { val currencyType = AnalyticsParam.CurrencyType.Currency(currency) - Analytics.send(Token.Bought(currencyType)) + Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value)) true } else { false diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 2879f9ef13..da1c6cc68a 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.store import timber.log.Timber @@ -12,7 +12,7 @@ import timber.log.Timber */ class SellCurrencyIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { return try { val intentData = intent?.data ?: return false val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false @@ -21,7 +21,7 @@ class SellCurrencyIntentHandler : IntentHandler { val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatchWithMain( + store.dispatchOnMain( TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index 95d2e3abe7..23f8ec2235 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -15,7 +15,7 @@ import java.net.URLDecoder */ class WalletConnectLinkIntentHandler : IntentHandler { - override suspend fun handleIntent(intent: Intent?): Boolean { + override fun handleIntent(intent: Intent?): Boolean { val intentData = intent?.data ?: return false val scheme = intent.scheme ?: return false @@ -34,7 +34,7 @@ class WalletConnectLinkIntentHandler : IntentHandler { Timber.e(e) return false } - store.dispatchWithMain(WalletConnectAction.HandleDeepLink(decodedWcUri)) + store.dispatchOnMain(WalletConnectAction.HandleDeepLink(decodedWcUri)) true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index b27217cca3..5b24d36caa 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -17,6 +17,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.saveWallet.redux.SaveWalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber @@ -73,40 +74,39 @@ object OnboardingHelper { backupCardsIds: List? = null, ) { Analytics.setContext(scanResponse) - when { - // When should save user wallets, then save card without navigate to save wallet screen - preferencesStorage.shouldSaveUserWallets -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + scope.launch { + when { + // When should save user wallets, then save card without navigate to save wallet screen + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { + proceedWithScanResponse(scanResponse, backupCardsIds) - store.dispatchOnMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - store.dispatchOnMain(SaveWalletAction.Save) - } - // When should not save user wallets but device has biometry and save wallet screen has not been shown, - // then open save wallet screen - tangemSdkManager.canUseBiometry && - preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + store.dispatchOnMain( + SaveWalletAction.ProvideBackupInfo( + scanResponse = scanResponse, + accessCode = accessCode, + backupCardsIds = backupCardsIds?.toSet(), + ), + ) + store.dispatchOnMain(SaveWalletAction.Save) + } + // When should not save user wallets but device has biometry and save wallet screen has not been shown, + // then open save wallet screen + tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { + proceedWithScanResponse(scanResponse, backupCardsIds) - delay(timeMillis = 1_200) + delay(timeMillis = 1_200) - store.dispatchOnMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) - } - // If device has no biometry and save wallet screen has been shown, then go through old scenario - else -> scope.launch { - proceedWithScanResponse(scanResponse, backupCardsIds) + store.dispatchOnMain( + SaveWalletAction.ProvideBackupInfo( + scanResponse = scanResponse, + accessCode = accessCode, + backupCardsIds = backupCardsIds?.toSet(), + ), + ) + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) + } + // If device has no biometry and save wallet screen has been shown, then go through old scenario + else -> proceedWithScanResponse(scanResponse, backupCardsIds) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 711c5acc69..675eb82581 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -6,7 +6,6 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext -import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* @@ -26,6 +25,7 @@ import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 9a76813cab..faf34114d6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -7,13 +7,17 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.wallet.models.toCurrencies +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import com.tangem.tap.userTokensRepository +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -113,6 +117,7 @@ private fun handleOtherCardsAction(action: Action) { } scope.launch { + // TODO: Use new repo [REDACTED_JIRA] userTokensRepository.saveUserTokens( card = result.data.card, tokens = blockchainNetworks.toCurrencies(), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index f68fa7e806..441798d55f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.isLockedSync -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* @@ -29,6 +28,12 @@ import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog +import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -292,10 +297,14 @@ private fun handle(action: Action, dispatch: DispatchFunction) { OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } CreateTwinWalletMode.RecreateWallet -> { - if (preferencesStorage.shouldSaveUserWallets) { - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) - } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + scope.launch { + val walletsRepository = store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + + if (walletsRepository.shouldSaveUserWalletsSync()) { + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) + } else { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index a4b12b434c..70c389c322 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -5,10 +5,8 @@ import android.util.TypedValue import android.view.View import android.view.animation.OvershootInterpolator import androidx.annotation.LayoutRes -import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible -import androidx.transition.TransitionInflater import androidx.transition.TransitionManager import coil.load import com.tangem.Message @@ -59,11 +57,11 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun configureTransitions() { when (store.state.twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> super.configureTransitions() + CreateTwinWalletMode.CreateWallet -> { + super.configureTransitions() + } CreateTwinWalletMode.RecreateWallet -> { - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) + configureDefaultTransactions() } } } @@ -105,7 +103,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundWhite) + setStatusBarColor(R.color.background_primary) } private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = @@ -195,11 +193,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { ) btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = when (state.currentStep) { - is TwinCardsStep.WelcomeOnly -> null - is TwinCardsStep.Welcome -> AppCompatResources.getDrawable(requireContext(), R.drawable.ic_tangem_24) - else -> null - } + btnMainAction.icon = null btnMainAction.setOnClickListener { onMainButtonClick() } } @@ -347,7 +341,6 @@ class TwinsCardsFragment : BaseOnboardingFragment() { btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.TopUp) } - btnAlternativeAction.isVisible = true btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) btnAlternativeAction.setOnClickListener { @@ -355,7 +348,6 @@ class TwinsCardsFragment : BaseOnboardingFragment() { } } else { btnMainAction.setText(R.string.onboarding_button_receive_crypto) - btnMainAction.icon = null btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.ShowAddressInfoDialog) } @@ -365,6 +357,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { tvHeader.setText(R.string.onboarding_topup_title) tvBody.setText(R.string.onboarding_top_up_body) + btnMainAction.icon = null btnRefreshBalanceWidget.changeState(state.walletBalance.state) if (btnRefreshBalanceWidget.isShowing != true) { @@ -375,6 +368,9 @@ class TwinsCardsFragment : BaseOnboardingFragment() { mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), ) + mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList = + requireContext().resources.getColorStateList(R.color.onboarding_card_background, null) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt index 2a074debb7..60faf916d7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt @@ -12,7 +12,7 @@ import com.tangem.wallet.R */ object TwinningProcessNotCompletedDialog { fun create(context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context) + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) .setMessage(R.string.onboarding_twin_exit_warning) .setPositiveButton(R.string.warning_button_ok) { _, _ -> } .setOnDismissListener { store.dispatchDialogHide() } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 9446f718c1..ce2c8784a8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.net.Uri -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.CompletionResult import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class OnboardingWalletAction : Action { @@ -18,7 +18,7 @@ sealed class OnboardingWalletAction : Action { ) : OnboardingWalletAction() object Done : OnboardingWalletAction() - data class FinishOnboarding(val lifecycleCoroutineScope: LifecycleCoroutineScope) : OnboardingWalletAction() + data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction() object ResumeBackup : OnboardingWalletAction() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index ad3f9d9ff2..f76de961e7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -136,6 +136,7 @@ private fun handleWalletAction(action: Action) { } scope.launch { + // TODO: Use new repo [REDACTED_JIRA] userTokensRepository.saveUserTokens( card = result.data.card, tokens = blockchainNetworks.toCurrencies(), @@ -152,7 +153,7 @@ private fun handleWalletAction(action: Action) { if (scanResponse == null) { store.dispatch(NavigationAction.PopBackTo()) - store.dispatch(HomeAction.ReadCard(lifecycleCoroutineScope = action.lifecycleCoroutineScope)) + store.dispatch(HomeAction.ReadCard(scope = action.scope)) } else { val backupState = store.state.onboardingWalletState.backupState val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) @@ -237,7 +238,12 @@ private fun handleWallet2Action(action: OnboardingWallet2Action) { SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed } - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully(creationType)) + Analytics.send( + event = Onboarding.CreateWallet.WalletCreatedSuccessfully( + creationType = creationType, + seedPhraseLength = action.mnemonicComponents.size, + ), + ) val response = CreateWalletResponse( card = result.data.card, derivedKeys = result.data.derivedKeys, @@ -460,6 +466,23 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) } + userWalletsListManager.selectedUserWalletSync?.walletId?.let { + scope.launch { + userWalletsListManager.update( + userWalletId = it, + update = { wallet -> + wallet.copy( + scanResponse = updateScanResponseAfterBackup( + scanResponse = wallet.scanResponse, + backupState = backupState, + ), + ) + }, + ) + store.dispatchOnMain(GlobalAction.UpdateUserWalletsListManager(userWalletsListManager)) + } + } + val notActivatedCardIds = gatherCardIds(backupState, card) .mapNotNull { if (cardActivationIsFinished(it)) null else it } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 75d9b4dc87..bed2a5abf6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -12,7 +12,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuProvider import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.transition.TransitionInflater import androidx.transition.TransitionManager import by.kirich1409.viewbindingdelegate.viewBinding import coil.load @@ -22,8 +21,10 @@ import com.tangem.common.CardIdFormatter import com.tangem.common.CompletionResult import com.tangem.common.core.CardIdDisplayFormat import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse +import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter @@ -62,6 +63,8 @@ class OnboardingWalletFragment : internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer } + private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true } + private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler() private val seedPhraseViewModel by viewModels() @@ -71,12 +74,6 @@ class OnboardingWalletFragment : private lateinit var animator: BackupAnimator - override fun configureTransitions() { - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -158,6 +155,7 @@ class OnboardingWalletFragment : oldState.onboardingWalletState == newState.onboardingWalletState }.select { it.onboardingWalletState } } + setStatusBarColor(R.color.background_primary) } override fun onStop() { @@ -201,7 +199,7 @@ class OnboardingWalletFragment : } } - internal fun loadImageIntoImageView(uri: Uri?, view: ImageView) { + private fun loadImageIntoImageView(uri: Uri?, view: ImageView) { view.load(uri) { placeholder(R.drawable.card_placeholder_black) error(R.drawable.card_placeholder_black) @@ -268,7 +266,7 @@ class OnboardingWalletFragment : btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) } - btnWalletAlternativeAction.show(state.canSkipBackup) + btnWalletAlternativeAction.show(state.canSkipBackup && canSkipBackup) } animator.showBackupIntro(state) } @@ -422,7 +420,7 @@ class OnboardingWalletFragment : animator.showWriteBackupCard(state, cardNumber) } - internal fun showSuccess() = with(binding) { + private fun showSuccess() = with(binding) { toolbar.title = getString(R.string.onboarding_done_header) tvHeader.text = getText(R.string.onboarding_done_header) @@ -437,9 +435,7 @@ class OnboardingWalletFragment : layoutButtonsCommon.btnWalletAlternativeAction.hide() layoutButtonsCommon.btnWalletMainAction.setOnClickListener { showConfetti(false) - lifecycleScope.launch { - store.dispatch(OnboardingWalletAction.FinishOnboarding(lifecycleCoroutineScope = lifecycleScope)) - } + store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope)) } animator.showSuccess { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt index f8b2c0bf66..07a64cc81e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store @@ -9,7 +10,7 @@ import com.tangem.wallet.R object AddMoreBackupCardsDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_warning) setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added) setPositiveButton(R.string.common_continue) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt index ed4e32a0a4..5094c1f4e9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt @@ -2,13 +2,14 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store import com.tangem.wallet.R object BackupInProgressDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_warning) setMessage(R.string.onboarding_backup_exit_warning) setPositiveButton(R.string.warning_button_ok) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt index 557c955bb1..de833bc7f7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store @@ -9,7 +10,7 @@ import com.tangem.wallet.R object ConfirmDiscardingBackupDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.welcome_interrupted_backup_discard_title) setMessage(R.string.welcome_interrupted_backup_discard_message) setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt index 678c2ca2df..f0249cfcd2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.app.Dialog import android.content.Context -import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.store @@ -13,7 +13,7 @@ import com.tangem.wallet.R */ object InterruptOnboardingDialog { fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.onboarding_exit_alert_title)) setMessage(context.getString(R.string.onboarding_exit_alert_message)) setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt index c8e883cdb8..90458e6f63 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.redux.global.GlobalAction @@ -11,13 +12,13 @@ import com.tangem.wallet.R object ResetBackupCardDialog { fun create(context: Context, cardId: String): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_attention) setMessage(R.string.onboarding_linking_error_card_with_wallets) setPositiveButton(R.string.common_cancel) { _, _ -> Analytics.send(Onboarding.Backup.ResetCancelEvent) } - setNegativeButton(R.string.common_reset) { _, _ -> + setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ -> Analytics.send(Onboarding.Backup.ResetPerformEvent) store.dispatch(BackupAction.ResetBackupCard(cardId)) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index 6576a36fce..c7c4c30450 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog @@ -10,7 +11,7 @@ import com.tangem.wallet.R object UnfinishedBackupFoundDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_warning) setMessage(R.string.welcome_interrupted_backup_alert_message) setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 5305d35dfb..1a1bdd7dd9 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -107,7 +107,9 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(error)) } .doOnSuccess { - preferencesStorage.shouldSaveUserWallets = true + store.state.daggerGraphState.get(DaggerGraphState::walletsRepository) + .saveShouldSaveUserWallets(item = true) + // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { preferencesStorage.shouldSaveAccessCodes = true @@ -140,10 +142,7 @@ internal class SaveWalletMiddleware { store.dispatchWithMain(SaveWalletAction.Save.Error(TangemSdkError.ExceptionError(error))) return } - val manager = UserWalletsListManager.provideBiometricImplementation( - context = context, - tangemSdkManager = tangemSdkManager, - ) + val manager = UserWalletsListManager.provideBiometricImplementation(context) store.dispatchWithMain(GlobalAction.UpdateUserWalletsListManager(manager)) } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index ae186c4912..4c56a3b9b4 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -1,18 +1,26 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.rekotlin.StoreSubscriber +import javax.inject.Inject -internal class SaveWalletViewModel : ViewModel(), StoreSubscriber { +@HiltViewModel +internal class SaveWalletViewModel @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel(), StoreSubscriber { private val stateInternal = MutableStateFlow(SaveWalletScreenState()) val state: StateFlow = stateInternal @@ -22,10 +30,14 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber Unit) { Text( modifier = Modifier.fillMaxWidth(fraction = .7f), text = stringResource(R.string.save_user_wallet_agreement_notice), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 4c9eaf36a5..094136bb80 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -102,6 +102,7 @@ sealed class AmountAction : SendScreenAction { data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction() data class SetAmountError(val error: TapError?) : AmountAction() data class SetDecimalSeparator(val separator: String) : AmountAction() + data class HideBalance(val hide: Boolean) : AmountAction() } // Fee diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt index 576497222e..96cf7c9c6f 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.redux.AppState @@ -9,6 +10,10 @@ import com.tangem.tap.features.send.redux.* import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress import com.tangem.tap.features.send.redux.AddressVerifyAction.Error +import com.tangem.tap.mainScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -17,6 +22,8 @@ import org.rekotlin.DispatchFunction */ internal class AddressMiddleware { + private val addressValidator = AddressValidator() + fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) { when (action) { is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch) @@ -54,13 +61,13 @@ internal class AddressMiddleware { dispatch: (Action) -> Unit, ) { val sendState = appState?.sendState ?: return - val wallet = sendState.walletManager?.wallet ?: return + val walletManager = sendState.walletManager ?: return val address = sendState.addressState.normalFieldValue ?: return val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput verifyAddress( address = address, - wallet = wallet, + walletManager = walletManager, isUserInput = isUserInput, dispatch = dispatch, sourceType = sourceType, @@ -68,6 +75,38 @@ internal class AddressMiddleware { } private fun verifyAddress( + address: String, + walletManager: WalletManager, + sourceType: AddressEntered.SourceType?, + isUserInput: Boolean, + dispatch: (Action) -> Unit, + ) { + val wallet = walletManager.wallet + + mainScope.launch { + val failReason = withContext(Dispatchers.IO) { + addressValidator.validateAddress(walletManager, address) + } + + if (failReason == null) { + dispatchSuccessValidationActions( + address = address, + wallet = wallet, + sourceType = sourceType, + isUserInput = isUserInput, + dispatch = dispatch, + ) + } else { + dispatchFailedValidationActions( + failReason = failReason, + sourceType = sourceType, + dispatch = dispatch, + ) + } + } + } + + private fun dispatchSuccessValidationActions( address: String, wallet: Wallet, sourceType: AddressEntered.SourceType?, @@ -113,57 +152,49 @@ internal class AddressMiddleware { val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query? - val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress) - if (failReason == null) { - noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let { - dispatch(AmountAction.SetAmount(it, false)) - dispatch(AmountActionUi.CheckAmountToSend) - } - dispatch(SetWalletAddress(supposedAddress, isUserInput)) - dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null)) - dispatch(FeeAction.RequestFee) - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Success, - ), - ) - } - } else { - dispatch(SetAddressError(failReason)) - dispatch(TransactionExtrasAction.Release) - sourceType?.let { - Analytics.send( - event = AddressEntered( - sourceType = sourceType, - validationResult = AddressEntered.ValidationResult.Fail, - ), - ) - } + noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let { + dispatch(AmountAction.SetAmount(it, false)) + dispatch(AmountActionUi.CheckAmountToSend) + } + dispatch(SetWalletAddress(supposedAddress, isUserInput)) + dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null)) + dispatch(FeeAction.RequestFee) + sourceType?.let { + Analytics.send( + event = AddressEntered( + sourceType = sourceType, + validationResult = AddressEntered.ValidationResult.Success, + ), + ) } } - private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? { - return if (wallet.blockchain.validateAddress(address)) { - if (wallet.addresses.all { it.value != address }) { - null - } else { - Error.ADDRESS_SAME_AS_WALLET - } - } else { - Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN + private fun dispatchFailedValidationActions( + failReason: Error, + sourceType: AddressEntered.SourceType?, + dispatch: (Action) -> Unit, + ) { + dispatch(SetAddressError(failReason)) + dispatch(TransactionExtrasAction.Release) + sourceType?.let { + Analytics.send( + event = AddressEntered( + sourceType = sourceType, + validationResult = AddressEntered.ValidationResult.Fail, + ), + ) } } private fun String.removeShareUriQuery(): String = this.substringBefore("?") + private fun String.getQueryParameter(name: String): String? { return this.substringAfter("?").splitToMap("&", "=")[name] } private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) { val address = input ?: return - val wallet = appState?.sendState?.walletManager?.wallet ?: return + val walletManager = appState?.sendState?.walletManager ?: return val internalDispatcher: (Action) -> Unit = { when (it) { @@ -178,7 +209,7 @@ internal class AddressMiddleware { verifyAddress( address = address, - wallet = wallet, + walletManager = walletManager, sourceType = null, isUserInput = false, dispatch = internalDispatcher, diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt new file mode 100644 index 0000000000..4c99c1e5c8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AddressValidator.kt @@ -0,0 +1,52 @@ +package com.tangem.tap.features.send.redux.middlewares + +import com.tangem.blockchain.blockchains.near.NearWalletManager +import com.tangem.blockchain.blockchains.near.network.NearAccount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.extensions.Result +import com.tangem.tap.features.send.redux.AddressVerifyAction + +internal class AddressValidator { + + suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? { + val blockchain = walletManager.wallet.blockchain + val wallet = walletManager.wallet + return if ((blockchain == Blockchain.Near || blockchain == Blockchain.NearTestnet) && + address.length != NEAR_IMPLICIT_ADDRESS_LENGTH + ) { + validateNearAddress(walletManager, address) + } else { + validateAddress(wallet, address) + } + } + + private suspend fun validateNearAddress( + walletManager: WalletManager, + address: String, + ): AddressVerifyAction.Error? { + val result = (walletManager as? NearWalletManager)?.getAccount(address) + return if (result is Result.Success && result.data is NearAccount.Full) { + null + } else { + AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN + } + } + + private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? { + return if (wallet.blockchain.validateAddress(address)) { + if (wallet.addresses.all { it.value != address }) { + null + } else { + AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET + } + } else { + AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN + } + } + + companion object { + private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index eeb7c0cf20..0dfd830ce1 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin @@ -20,13 +19,15 @@ import com.tangem.domain.common.extensions.minimalAmount import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.analytics.events.Token.Send.SelectedCurrency.CurrencyType -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchErrorNotification +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -40,6 +41,11 @@ import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager +import com.tangem.tap.walletCurrenciesManager +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -174,33 +180,35 @@ private fun sendTransaction( transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) } scope.launch { - val updateWalletResult = walletManager.safeUpdate() - if (updateWalletResult is Result.Failure) { - withMainContext { - when (val error = updateWalletResult.error) { - is TapError -> store.dispatchErrorNotification(error) - is BlockchainSdkError -> { - updateFeedbackManagerInfo( - walletManager = walletManager, - amountToSend = amountToSend, - feeAmount = fee.amount, - destinationAddress = destinationAddress, - ) - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) - } - else -> { - val tapError = if (error.message == null) { - TapError.UnknownError - } else { - TapError.CustomError(error.message!!) - } - store.dispatchErrorNotification(tapError) - } - } - dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) - } - return@launch - } + // TODO: Risky commented this part, unknown logic, need to test if removed + // TODO: [REDACTED_JIRA] + // val updateWalletResult = walletManager.safeUpdate() + // if (updateWalletResult is Result.Failure) { + // withMainContext { + // when (val error = updateWalletResult.error) { + // is TapError -> store.dispatchErrorNotification(error) + // is BlockchainSdkError -> { + // updateFeedbackManagerInfo( + // walletManager = walletManager, + // amountToSend = amountToSend, + // feeAmount = fee.amount, + // destinationAddress = destinationAddress, + // ) + // dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) + // } + // else -> { + // val tapError = if (error.message == null) { + // TapError.UnknownError + // } else { + // TapError.CustomError(error.message!!) + // } + // store.dispatchErrorNotification(tapError) + // } + // } + // dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) + // } + // return@launch + // } val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk val linkedTerminalState = tangemSdk.config.linkedTerminal @@ -270,9 +278,7 @@ private fun sendTransaction( dispatch(NavigationAction.PopBackTo()) } scope.launch(Dispatchers.IO) { - updateWallet(walletManager) - delay(timeMillis = 11000) // more than 10000 to avoid throttling - updateWallet(walletManager) + updateAfterTransaction(walletManager) } } is SimpleResult.Failure -> { @@ -414,6 +420,19 @@ private fun updateWarnings(dispatch: (Action) -> Unit) { dispatch(SendAction.Warnings.Set(warnings)) } +private suspend fun updateAfterTransaction(walletManager: WalletManager) { + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + updateWalletsLegacy(walletManager) + } +} + +private suspend fun updateWalletsLegacy(walletManager: WalletManager) { + updateWallet(walletManager) + delay(timeMillis = 11000) // more than 10000 to avoid throttling + updateWallet(walletManager) +} + private suspend fun updateWallet(walletManager: WalletManager) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to update wallet, no user wallet selected") diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index 9221d5ef60..1f3b82b267 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.send.redux.reducers +import com.tangem.common.Strings.STARS import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.features.send.redux.AmountAction @@ -38,8 +39,8 @@ class AmountReducer : SendInternalReducer { val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString()) state.copy( viewAmountValue = InputViewValue(viewValue), - viewBalanceValue = rescaledBalance.stripZeroPlainString(), - mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched), + viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(), + mainCurrency = state.createMainCurrency(currency, true), maxLengthOfAmount = sendState.getDecimals(currency), cursorAtTheSamePosition = false, ) @@ -80,6 +81,18 @@ class AmountReducer : SendInternalReducer { } is AmountAction.SetAmountError -> state.copy(error = action.error) is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator) + is AmountAction.HideBalance -> { + val rescaledBalance = if (state.mainCurrency.type == MainCurrencyType.CRYPTO) { + state.balanceCrypto + } else { + sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) + } + + state.copy( + hideBalance = action.hide, + viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(), + ) + } } return updateLastState(sendState.copy(amountState = result), result) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 8a9cab2329..f2d05ab0c2 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -53,6 +53,7 @@ private class SendReducer : SendInternalReducer { is SendAction.Dialog.Hide -> sendState.copy(dialog = null) is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList) is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState) + is SendAction.SendSuccess -> sendState.copy(isSuccessSend = true) else -> return sendState } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index 8400f07c87..34553ec931 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -43,6 +43,7 @@ data class SendState( val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED), val dialog: StateDialog? = null, val externalTransactionData: ExternalTransactionData? = null, + val isSuccessSend: Boolean = false, ) : SendScreenState { override val stateId: StateId = StateId.SEND_SCREEN @@ -130,6 +131,7 @@ data class AmountState( val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code), val amountToSendCrypto: BigDecimal = BigDecimal.ZERO, val balanceCrypto: BigDecimal = BigDecimal.ZERO, + val hideBalance: Boolean = false, val cursorAtTheSamePosition: Boolean = true, val maxLengthOfAmount: Int = 2, val decimalSeparator: String = ".", diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 0e963d3ac5..e115eff66d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -11,6 +11,7 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding @@ -45,6 +46,7 @@ import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentSendBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* @@ -56,8 +58,11 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L [REDACTED_AUTHOR] */ @OptIn(FlowPreview::class) +@AndroidEntryPoint class SendFragment : BaseStoreFragment(R.layout.fragment_send) { + private val viewModel by viewModels() + lateinit var sendBtn: ViewStateWidget private lateinit var etAmountToSend: TextInputEditText @@ -70,6 +75,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + sendSubscriber.initViewModel(viewModel) Analytics.send(Token.Send.ScreenOpened()) } @@ -348,6 +355,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun onDestroy() { store.dispatch(ReleaseSendState) + lifecycle.removeObserver(viewModel) super.onDestroy() } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt new file mode 100644 index 0000000000..806cb70f74 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -0,0 +1,96 @@ +package com.tangem.tap.features.send.ui + +import androidx.lifecycle.* +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.send.navigation.SendRouter +import com.tangem.tap.di.DelayedWork +import com.tangem.tap.features.send.redux.AmountAction +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +@HiltViewModel +internal class SendViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val appStateHolder: AppStateHolder, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + @DelayedWork private val coroutineScope: CoroutineScope, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver { + + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + + override fun onCreate(owner: LifecycleOwner) { + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { isBalanceHidden -> + withContext(dispatchers.main) { + appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden)) + } + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + } + + fun updateCurrencyDelayed() { + if (cryptoCurrency != null) { + coroutineScope.launch { + getSelectedWalletSyncUseCase() + .fold( + ifLeft = { Timber.e(it.toString()) }, + ifRight = { wallet -> + // we should update network to find pending tx after 1 sec + updateForPendingTx(wallet, cryptoCurrency.network) + // we should update network for new balance + updateForBalance(wallet, cryptoCurrency.network) + }, + ) + } + } else { + Timber.w("$TAG: cryptoCurrency is null, legacy flow") + } + } + + private suspend fun updateForPendingTx(userWallet: UserWallet, network: Network) { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(network)) + } + + private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = network, + delayMillis = UPDATE_BALANCE_DELAY_MILLIS, + refresh = true, + ) + } + + companion object { + private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + private const val TAG = "SendViewModel" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index e05278fceb..d71adc7cc8 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -18,6 +18,7 @@ import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.send.ui.SendViewModel import com.tangem.tap.features.send.ui.dialogs.* import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.utils.ROUGH_SIGN @@ -29,13 +30,23 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ @Suppress("LargeClass") -class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber(fragment) { +internal class SendStateSubscriber( + fragment: BaseStoreFragment, +) : FragmentStateSubscriber(fragment) { private var dialog: Dialog? = null + private var sendViewModel: SendViewModel? = null + fun initViewModel(viewModel: SendViewModel) { + sendViewModel = viewModel + } override fun updateWithNewState(fg: BaseStoreFragment, state: SendState) { fg.view ?: return if (fg !is SendFragment) return + if (state.isSuccessSend) { + sendViewModel?.updateCurrencyDelayed() + return + } val lastChangedStates = state.lastChangedStates.toList() state.lastChangedStates.clear() @@ -260,7 +271,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) - val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray + val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary tvAmountCurrency.setTextColor(fg.getColor(textColor)) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt index 7ff7b78228..114990f05b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/DefaultTokensListRepository.kt @@ -6,26 +6,26 @@ import androidx.paging.PagingData import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.domain.TokensListRepository import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow /** * Default repository implementation of tokens list feature * - * @property tangemTechApi Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property testnetTokensStorage storage for getting testnet tokens data + * @property tangemTechApi Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + * @property testnetTokensStorage storage for getting testnet tokens data * [REDACTED_AUTHOR] */ internal class DefaultTokensListRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val testnetTokensStorage: TestnetTokensStorage, ) : TokensListRepository { @@ -37,16 +37,23 @@ internal class DefaultTokensListRepository( enablePlaceholders = false, ), pagingSourceFactory = { - if (reduxStateHolder.scanResponse?.card?.isTestCard == true) { - TestnetTokensPagingSource(testnetTokensStorage, searchText) - } else { - TangemApiTokensPagingSource( - api = tangemTechApi, - dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, - searchText = searchText, - ) - } + val defaultSource = TangemApiTokensPagingSource( + api = tangemTechApi, + dispatchers = dispatchers, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + searchText = searchText, + ) + + getSelectedWalletSyncUseCase().fold( + ifLeft = { defaultSource }, + ifRight = { + if (it.scanResponse.card.isTestCard) { + TestnetTokensPagingSource(testnetTokensStorage, searchText) + } else { + defaultSource + } + }, + ) }, ).flow } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt index fb04538ee2..5a65f98b6b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/data/TangemApiTokensPagingSource.kt @@ -7,24 +7,24 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.extensions.supportedBlockchains import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching /** * Paging source that get tokens by Tangem Tech API * - * @property api Tangem Tech API - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @property searchText search text + * @property api Tangem Tech API + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + * @property searchText search text */ internal class TangemApiTokensPagingSource( private val api: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val searchText: String?, ) : PagingSource() { @@ -39,9 +39,10 @@ internal class TangemApiTokensPagingSource( val page = params.key ?: 0 return runCatching(dispatchers.io) { - val scanResponse = reduxStateHolder.scanResponse - val supportedBlockchains = scanResponse?.card?.supportedBlockchains(scanResponse.cardTypesResolver) - ?: Blockchain.values().toList() + val supportedBlockchains = getSelectedWalletSyncUseCase().fold( + ifLeft = { Blockchain.values().toList() }, + ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) }, + ) api.getCoins( networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt index ddc7a2a15b..464420346b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt @@ -2,10 +2,10 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,17 +25,16 @@ internal object TokensListInteractorModule { fun provideTokensListInteractor( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListInteractor { return DefaultTokensListInteractor( repository = DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, testnetTokensStorage = testnetTokensStorage, ), - reduxStateHolder = reduxStateHolder, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt index dbba8fe0dd..186545138b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRepositoryModule.kt @@ -2,9 +2,9 @@ package com.tangem.tap.features.tokens.impl.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.testnet.TestnetTokensStorage +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository import com.tangem.tap.features.tokens.impl.domain.TokensListRepository -import com.tangem.tap.proxy.AppStateHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,13 +24,13 @@ internal object TokensListRepositoryModule { fun providesTokensListRepository( tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, - reduxStateHolder: AppStateHolder, + getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, testnetTokensStorage: TestnetTokensStorage, ): TokensListRepository { return DefaultTokensListRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, - reduxStateHolder = reduxStateHolder, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, testnetTokensStorage = testnetTokensStorage, ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt index 07a7a3412d..2a7da5cca4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.tokens.impl.di -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import dagger.Module @@ -18,7 +17,5 @@ internal object TokensListRouterModule { @Provides @ViewModelScoped - fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter { - return DefaultTokensListRouter(customTokenFeatureToggles) - } + fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt index 64bd577f52..c470316894 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt @@ -1,262 +1,17 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.CompletionResult -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.common.extensions.guard -import com.tangem.common.extensions.toMapKey -import com.tangem.common.flatMap -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.configs.CardConfig -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.* -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import timber.log.Timber /** * Default implementation of tokens list interactor - * FIXME("Necessary to avoid using redux actions") * - * @property repository repository of tokens list feature - * @property reduxStateHolder redux state holder + * @property repository repository of tokens list feature */ -internal class DefaultTokensListInteractor( - private val repository: TokensListRepository, - private val reduxStateHolder: AppStateHolder, -) : TokensListInteractor { +internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor { override fun getTokensList(searchText: String): Flow> { return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null })) } - - override suspend fun saveChanges(tokens: List, blockchains: List) { - val scanResponse = requireNotNull(reduxStateHolder.scanResponse) - - val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle() - val currentTokens = store.state.tokensState.addedWallets - .toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle) - - val currentBlockchains = store.state.tokensState.addedWallets - .toNonCustomBlockchains(derivationStyle = derivationStyle) - - val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains) - - val tokensToAdd = tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } } - - val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() - val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() - if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { - store.dispatchDebugErrorNotification(message = "Nothing to save") - return - } - - remove( - tokens = tokensToRemove, - blockchains = blockchainsToRemove, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse) - } - - private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, - ): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain) - } - .distinct() - } - - private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - (currency as? Currency.Blockchain)?.blockchain - } - .distinct() - } - - private suspend fun remove( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ) { - val currencies = convertToCurrencies(tokens, blockchains, derivationStyle) - if (currencies.isEmpty()) return - - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to remove currencies, no user wallet selected") - return - } - - walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies) - } - - private suspend fun add( - tokens: List, - blockchains: List, - scanResponse: ScanResponse, - ) { - val currenciesToAdd = convertToCurrencies( - tokens = tokens, - blockchains = blockchains, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - // TODO("[REDACTED_TASK_KEY] use DerivationManager") - if (scanResponse.supportsHdWallet()) { - deriveMissingBlockchains(scanResponse, currenciesToAdd) - } else { - submitAdd(scanResponse, currenciesToAdd) - return - } - } - - private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List) { - val config = CardConfig.createConfig(scanResponse.card) - val derivationDataList = currencies.mapNotNull { currency -> - val curve = config.primaryCurve(currency.blockchain) - curve?.let { getDerivations(curve, scanResponse, currency) } - } - - val derivations = buildMap> { - derivationDataList.forEach { - val current = this[it.derivations.first] - if (current != null) { - current.addAll(it.derivations.second) - current.distinct() - } else { - this[it.derivations.first] = it.derivations.second.toMutableList() - } - } - } - if (derivations.isEmpty()) { - submitAdd(scanResponse, currencies) - return - } - - when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) { - is CompletionResult.Success -> { - val newDerivedKeys = result.data.entries - val oldDerivedKeys = scanResponse.derivedKeys - - val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() - - val updatedDerivedKeys = walletKeys.associateWith { walletKey -> - val oldDerivations = ExtendedPublicKeysMap(map = oldDerivedKeys[walletKey] ?: emptyMap()) - val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(map = emptyMap()) - ExtendedPublicKeysMap(map = oldDerivations + newDerivations) - } - - val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) - - store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) - delay(DELAY_SDK_DIALOG_CLOSE) - - submitAdd(scanResponse, currencies) - return - } - is CompletionResult.Failure -> { - store.dispatchDebugErrorNotification(TapError.CustomError(customMessage = "Error adding tokens")) - } - } - } - - private fun getDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - currency: Currency, - ): TokensMiddleware.DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - - val supportedCurves = currency.blockchain.getSupportedCurves() - val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - .takeIf { supportedCurves.contains(curve) } - - val customPath = currency.derivationPath?.let { - DerivationPath(it) - }.takeIf { supportedCurves.contains(curve) } - - val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() - if (bothCandidates.isEmpty()) return null - - if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) { - currency.derivationPath?.let { - bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) - } - } - - val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() - val alreadyDerivedKeys: ExtendedPublicKeysMap = - scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) - val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() - - val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } - if (toDerive.isEmpty()) return null - - return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) - } - - private suspend fun submitAdd(scanResponse: ScanResponse, currencies: List) { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to add currencies, no user wallet selected") - return - } - - userWalletsListManager - .update( - userWalletId = selectedUserWallet.walletId, - update = { userWallet -> userWallet.copy(scanResponse = scanResponse) }, - ) - .flatMap { updatedUserWallet -> - walletCurrenciesManager.addCurrencies( - userWallet = updatedUserWallet, - currenciesToAdd = currencies, - ) - } - } - - private fun convertToCurrencies( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ): List { - return tokens.map { tokenWithBlockchain -> - Currency.Token( - token = tokenWithBlockchain.token, - blockchain = tokenWithBlockchain.blockchain, - derivationPath = tokenWithBlockchain.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }.plus( - blockchains.map { blockchain -> - Currency.Blockchain( - blockchain = blockchain, - derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }, - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt index fee08220bf..c363dfc3c2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.common.Blockchain import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain import kotlinx.coroutines.flow.Flow /** @@ -15,12 +13,4 @@ internal interface TokensListInteractor { /** Get tokens list using filter by text [searchText] */ fun getTokensList(searchText: String): Flow> - - /** - * Save added tokens - * - * @param tokens tokens list that need to save - * @param blockchains blockchains list that need to save - */ - suspend fun saveChanges(tokens: List, blockchains: List) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index 417a308de0..a6f68a2097 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -1,22 +1,18 @@ package com.tangem.tap.features.tokens.impl.presentation -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel -import androidx.transition.TransitionInflater import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel -import com.tangem.wallet.R import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject /** * Fragment with list of tokens @@ -24,33 +20,23 @@ import dagger.hilt.android.AndroidEntryPoint [REDACTED_AUTHOR] */ @AndroidEntryPoint -internal class TokensListFragment : Fragment() { +internal class TokensListFragment : ComposeFragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - with(TransitionInflater.from(requireContext())) { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Composable + override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel().apply { + LocalLifecycleOwner.current.lifecycle.addObserver(this) } - - return ComposeView(inflater.context).apply { - setContent { - isTransitionGroup = true - - val viewModel = hiltViewModel().apply { - LocalLifecycleOwner.current.lifecycle.addObserver(this) - } - - TangemTheme { - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(color = statusBarColor) - } - TokensListScreen( - modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, - ) - } - } + val statusBarColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(color = statusBarColor) } + TokensListScreen( + modifier = Modifier.systemBarsPadding(), + stateHolder = viewModel.uiState, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt deleted file mode 100644 index 207976d628..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.models - -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.store - -/** - * Required data for tokens list screen - * FIXME("Necessary to avoid using redux state") - * -[REDACTED_AUTHOR] - */ -class TokensListArgs { - /** Tokens list screen mode */ - val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess - - /** Tokens list that accessible from the main screen */ - val mainScreenTokenList: List get() = store.state.tokensState.addedTokens - - /** Blockchains list that accessible from the main screen */ - val mainScreenBlockchainList: List get() = store.state.tokensState.addedBlockchains -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 93ae4e1b17..68b277ea7e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -6,8 +6,6 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -18,20 +16,14 @@ import com.tangem.wallet.R * [REDACTED_AUTHOR] */ -internal class DefaultTokensListRouter( - private val customTokenFeatureToggles: CustomTokenFeatureToggles, -) : TokensListRouter { +internal class DefaultTokensListRouter : TokensListRouter { override fun popBackStack() { store.dispatch(NavigationAction.PopBackTo()) } override fun openAddCustomTokenScreen() { - if (customTokenFeatureToggles.isRedesignedScreenEnabled) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) - } else { - store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) - } + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) } override fun showAddressCopiedNotification() { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt index 104ebf9d8c..ea2c77ed2a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt @@ -3,21 +3,21 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key 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.res.painterResource -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import kotlinx.collections.immutable.ImmutableCollection @@ -70,11 +70,21 @@ internal fun BriefNetworksList( */ @Composable internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) { + val isAdded = model is NetworkItemState.ManageContent && model.isAdded.value Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) { - Image( + if (!isAdded) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size20) + .clip(CircleShape) + .background(TangemTheme.colors.control.unchecked), + ) + } + Icon( painter = painterResource(id = model.iconResId.value), contentDescription = null, modifier = Modifier.size(size = TangemTheme.dimens.size20), + tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, ) if (model.isMainNetwork) { @@ -83,14 +93,14 @@ internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modi .align(Alignment.TopEnd) .size(TangemTheme.dimens.size7) .clip(CircleShape) - .background(TangemColorPalette.White), + .background(TangemTheme.colors.background.primary), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier .size(TangemTheme.dimens.size5) .clip(CircleShape) - .background(TangemColorPalette.Meadow), + .background(TangemTheme.colors.icon.accent), ) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt index bfe5cb5d3b..806c161a01 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/DetailedNetworksList.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState @@ -95,7 +94,10 @@ private fun DetailedNetworkItem(token: TokenItemState, network: NetworkItemState }, modifier = Modifier.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing8), colors = SwitchDefaults.colors( - checkedThumbColor = TangemColorPalette.Meadow, + checkedThumbColor = TangemTheme.colors.control.key, + checkedTrackColor = TangemTheme.colors.icon.accent, + uncheckedThumbColor = TangemTheme.colors.control.key, + uncheckedTrackColor = TangemTheme.colors.icon.informative, ), ) } @@ -112,7 +114,11 @@ private fun RowScope.NetworkTitle(model: NetworkItemState) { withStyle( style = SpanStyle( fontWeight = FontWeight.Normal, - color = if (model.isMainNetwork) TangemColorPalette.Meadow else TangemColorPalette.Dark2, + color = if (model.isMainNetwork) { + TangemTheme.colors.icon.accent + } else { + TangemTheme.colors.icon.secondary + }, ), ) { append(text = model.protocolName) @@ -121,16 +127,16 @@ private fun RowScope.NetworkTitle(model: NetworkItemState) { fontWeight = FontWeight.SemiBold, fontSize = 13.sp, color = if (model is NetworkItemState.ManageContent && model.isAdded.value) { - TangemColorPalette.Black + TangemTheme.colors.text.primary1 } else { - TangemColorPalette.Dark2 + TangemTheme.colors.text.secondary }, ) } @Preview @Composable -private fun Preview_DetailedNetworksList_ManageAccess() { +private fun Preview_DetailedNetworksList_ManageAccess_Light() { TangemTheme { DetailedNetworksList( isExpanded = true, @@ -142,7 +148,31 @@ private fun Preview_DetailedNetworksList_ManageAccess() { @Preview @Composable -private fun Preview_DetailedNetworksList_ReadAccess() { +private fun Preview_DetailedNetworksList_ManageAccess_Dark() { + TangemTheme { + DetailedNetworksList( + isExpanded = true, + token = TokenListPreviewData.createManageToken(), + networks = TokenListPreviewData.createManageNetworksList(), + ) + } +} + +@Preview +@Composable +private fun Preview_DetailedNetworksList_ReadAccess_Light() { + TangemTheme { + DetailedNetworksList( + isExpanded = true, + token = TokenListPreviewData.createReadToken(), + networks = TokenListPreviewData.createReadNetworksList(), + ) + } +} + +@Preview +@Composable +private fun Preview_DetailedNetworksList_ReadAccess_Dark() { TangemTheme { DetailedNetworksList( isExpanded = true, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt index 6e9be1c229..671961ab61 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/NetworkItemArrow.kt @@ -1,12 +1,7 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -45,8 +40,24 @@ internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) { @Preview @Composable -private fun Preview_NetworkItemArrow_Column() { - TangemTheme { +private fun Preview_NetworkItemArrow_Column_Light() { + TangemTheme(isDark = false) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary) + .padding(start = TangemTheme.dimens.size36), + ) { + NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false) + NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true) + } + } +} + +@Preview +@Composable +private fun Preview_NetworkItemArrow_Column_Dark() { + TangemTheme(isDark = true) { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt index 46fc3f7554..862b0c874f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokenItem.kt @@ -1,27 +1,17 @@ package com.tangem.tap.features.tokens.impl.presentation.ui -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.with -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -33,20 +23,25 @@ import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.CurrencyPlaceholderIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import com.tangem.tap.common.compose.extensions.toPx import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.wallet.R +import kotlinx.coroutines.launch /** [REDACTED_AUTHOR] */ +@Suppress("LongMethod") @Composable internal fun TokenItem(model: TokenItemState) { var isExpanded by rememberSaveable { mutableStateOf(value = false) } + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } ConstraintLayout( modifier = Modifier .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary) .padding(top = TangemTheme.dimens.spacing16), ) { val (icon, title, availableNetworksText) = createRefs() @@ -55,14 +50,24 @@ internal fun TokenItem(model: TokenItemState) { val spacing16 = TangemTheme.dimens.spacing16 val spacing6 = TangemTheme.dimens.spacing6 - Icon( - name = model.fullName, - iconUrl = model.iconUrl, - modifier = Modifier.constrainAs(icon) { - top.linkTo(parent.top) - start.linkTo(anchor = parent.start, margin = spacing16) - }, - ) + Box( + modifier = Modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ) + .size(TangemTheme.dimens.size46) + .constrainAs(icon) { + top.linkTo(parent.top) + start.linkTo(anchor = parent.start, margin = spacing16) + }, + ) { + Icon( + name = model.fullName, + iconUrl = model.iconUrl, + onContrastCalculate = { iconBackgroundColor = it }, + ) + } Title( title = model.fullName, @@ -119,8 +124,11 @@ internal fun TokenItem(model: TokenItemState) { } @Composable -private fun Icon(name: String, iconUrl: String, modifier: Modifier = Modifier) { +private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) { val iconModifier = modifier.size(size = TangemTheme.dimens.size46) + val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb() + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() SubcomposeAsyncImage( modifier = iconModifier, @@ -128,6 +136,20 @@ private fun Icon(name: String, iconUrl: String, modifier: Modifier = Modifier) { .size(size = TangemTheme.dimens.size46.toPx().toInt()) .data(data = iconUrl) .crossfade(enable = true) + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = screenBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + onContrastCalculate(color) + } + } + }, + ) .build(), contentDescription = null, loading = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) }, @@ -183,18 +205,34 @@ private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, m } } -@Preview +@Preview(showBackground = true) @Composable -private fun Preview_TokenItem_ManageAccess() { - TangemTheme { +private fun Preview_TokenItem_ManageAccess_Light() { + TangemTheme(isDark = false) { TokenItem(model = TokenListPreviewData.createManageToken()) } } -@Preview +@Preview() @Composable -private fun Preview_TokenItem_ReadAccess() { - TangemTheme { +private fun Preview_TokenItem_ManageAccess_Dark() { + TangemTheme(isDark = true) { + TokenItem(model = TokenListPreviewData.createManageToken()) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview_TokenItem_ReadAccess_Light() { + TangemTheme(isDark = false) { + TokenItem(model = TokenListPreviewData.createReadToken()) + } +} + +@Preview() +@Composable +private fun Preview_TokenItem_ReadAccess_Dark() { + TangemTheme(isDark = true) { TokenItem(model = TokenListPreviewData.createReadToken()) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index ea6ab16083..b28517d8a1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -24,6 +22,8 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType @@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp import androidx.paging.PagingData import androidx.paging.compose.* import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder @@ -70,24 +69,25 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi } }, floatingActionButtonPosition = FabPosition.Center, + backgroundColor = TangemTheme.colors.background.primary, ) { scaffoldPadding -> val tokens = stateHolder.tokens.collectAsLazyPagingItems() TokensListContent( - isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, + isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible && !stateHolder.isLoading, tokens = tokens, scaffoldPadding = scaffoldPadding, bottomMarginDp = floatingButtonHeight, ) - stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh) + Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") { + if (it) { + LoadingContent() + } + } - AnimatedVisibility( - visible = stateHolder.isLoading, - enter = fadeIn(), - exit = fadeOut(), - ) { - LoadingContent() + LaunchedEffect(key1 = tokens.loadState.refresh) { + stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh) } } } @@ -97,10 +97,10 @@ private fun LoadingContent() { Box( modifier = Modifier .fillMaxSize() - .background(color = TangemColorPalette.White), + .background(color = TangemTheme.colors.background.primary), contentAlignment = Alignment.Center, ) { - CircularProgressIndicator(color = TangemColorPalette.Meadow) + CircularProgressIndicator(color = TangemTheme.colors.icon.accent) } } @@ -144,19 +144,19 @@ private fun DifferentAddressesWarning() { modifier = Modifier .padding(TangemTheme.dimens.spacing16) .background( - color = TangemColorPalette.Light1, + color = TangemTheme.colors.button.disabled, shape = RoundedCornerShape(TangemTheme.dimens.radius10), ), contentAlignment = Alignment.Center, ) { - val text = stringResource(id = R.string.alert_manage_tokens_addresses_message) + val text = stringResource(id = R.string.warning_manage_tokens_legacy_derivation_message) Text( text = text, modifier = Modifier.padding( horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing8, ), - color = TangemColorPalette.Dark1, + color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Start, style = TangemTheme.typography.body2.copy( letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp), @@ -178,73 +178,71 @@ private fun SaveChangesButton(onClick: () -> Unit, modifier: Modifier = Modifier ) } -@Preview +@Preview(showSystemUi = true) @Composable -private fun Preview_TokensListScreen_Loading() { - TangemTheme { - TokensListScreen( - stateHolder = TokensListStateHolder.ReadContent( - toolbarState = TokensListToolbarState.Title.Manage( - titleResId = R.string.main_manage_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - onAddCustomTokenClick = {}, - ), - isLoading = true, - isDifferentAddressesBlockVisible = false, - tokens = emptyFlow(), - onTokensLoadStateChanged = {}, - ), - ) +private fun Preview_TokensListScreen_Light( + @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, +) { + TangemTheme(isDark = false) { + TokensListScreen(stateHolder) } } -@Preview +@Preview(showSystemUi = true) @Composable -private fun Preview_TokensListScreen_Manage() { - TangemTheme { - TokensListScreen( - stateHolder = TokensListStateHolder.ManageContent( - toolbarState = TokensListToolbarState.Title.Manage( - titleResId = R.string.main_manage_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - onAddCustomTokenClick = {}, - ), - isLoading = false, - isDifferentAddressesBlockVisible = true, - tokens = flowOf( - PagingData.from( - listOf(TokenListPreviewData.createManageToken()), - ), - ), - onSaveButtonClick = {}, - onTokensLoadStateChanged = {}, - ), - ) +private fun Preview_TokensListScreen_Dark( + @PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder, +) { + TangemTheme(isDark = true) { + TokensListScreen(stateHolder) } } -@Preview -@Composable -private fun Preview_TokensListScreen_Read() { - TangemTheme { - TokensListScreen( - stateHolder = TokensListStateHolder.ReadContent( - toolbarState = TokensListToolbarState.Title.Read( - titleResId = R.string.common_search_tokens, - onBackButtonClick = {}, - onSearchButtonClick = {}, - ), - isLoading = false, - isDifferentAddressesBlockVisible = false, - tokens = flowOf( - PagingData.from( - listOf(TokenListPreviewData.createManageToken()), - ), - ), - onTokensLoadStateChanged = {}, +private class TokensListScreenProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokensListStateHolder.ReadContent( + toolbarState = TokensListToolbarState.Title.Manage( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, ), - ) - } -} \ No newline at end of file + isLoading = true, + isDifferentAddressesBlockVisible = false, + tokens = emptyFlow(), + onTokensLoadStateChanged = {}, + ), + TokensListStateHolder.ManageContent( + toolbarState = TokensListToolbarState.Title.Manage( + titleResId = R.string.main_manage_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + onAddCustomTokenClick = {}, + ), + isLoading = false, + isDifferentAddressesBlockVisible = true, + tokens = flowOf( + PagingData.from( + listOf(TokenListPreviewData.createManageToken()), + ), + ), + onSaveButtonClick = {}, + onTokensLoadStateChanged = {}, + ), + TokensListStateHolder.ReadContent( + toolbarState = TokensListToolbarState.Title.Read( + titleResId = R.string.common_search_tokens, + onBackButtonClick = {}, + onSearchButtonClick = {}, + ), + isLoading = false, + isDifferentAddressesBlockVisible = false, + tokens = flowOf( + PagingData.from( + listOf(TokenListPreviewData.createManageToken()), + ), + ), + onTokensLoadStateChanged = {}, + ), + ), +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index f5db5aa1a1..71aba31a33 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.tokens.impl.presentation.ui +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -7,10 +8,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi @@ -26,7 +24,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.InputField @@ -39,12 +36,20 @@ import kotlinx.coroutines.delay */ @Composable internal fun TokensListToolbar(state: TokensListToolbarState) { - TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) { + val toolbarElevation = if (isSystemInDarkTheme()) { + TangemTheme.dimens.elevation0 + } else { + AppBarDefaults.TopAppBarElevation + } + TopAppBar( + backgroundColor = TangemTheme.colors.background.secondary, + elevation = toolbarElevation, + ) { IconButton(onClick = state.onBackButtonClick) { Icon( painter = painterResource(id = R.drawable.ic_back_24), contentDescription = "Go back", - tint = TangemTheme.colors.icon.secondary, + tint = TangemTheme.colors.icon.primary1, ) } @@ -69,7 +74,7 @@ private fun TitleContent(state: Title, modifier: Modifier = Modifier) { Icon( painter = painterResource(id = R.drawable.ic_search_24), contentDescription = "Search", - tint = TangemTheme.colors.icon.secondary, + tint = TangemTheme.colors.icon.primary1, ) } @@ -78,7 +83,7 @@ private fun TitleContent(state: Title, modifier: Modifier = Modifier) { Icon( painter = painterResource(id = R.drawable.ic_plus_24), contentDescription = "Add custom token", - tint = TangemTheme.colors.icon.secondary, + tint = TangemTheme.colors.icon.primary1, ) } } @@ -94,7 +99,10 @@ private fun InputContent(state: InputField, modifier: Modifier = Modifier) { value = state.value, onValueChange = state.onValueChange, modifier = modifier.focusRequester(focusRequester), - textStyle = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.Normal), + textStyle = TangemTheme.typography.subtitle1.copy( + fontWeight = FontWeight.Normal, + color = TangemTheme.colors.text.primary1, + ), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }), singleLine = true, @@ -146,7 +154,7 @@ private fun Hint(value: String) { Icon( painter = painterResource(id = R.drawable.ic_search_24), contentDescription = null, - tint = TangemColorPalette.Dark1, + tint = TangemTheme.colors.icon.secondary, ) Text( text = stringResource(id = R.string.common_search), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt new file mode 100644 index 0000000000..da83face06 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.TokenWithBlockchain + +internal data class TokensListCryptoCurrencies( + val coins: List, + val tokens: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt new file mode 100644 index 0000000000..d4af20b4ea --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -0,0 +1,187 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.store +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Class that divide a new and legacy logic when user uses tokens list screen + * + * @property walletFeatureToggles wallet feature toggles + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + * @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet + */ +internal class TokensListMigration( + private val walletFeatureToggles: WalletFeatureToggles, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, +) { + + private var currentNewCoins: List by Delegates.notNull() + private var currentNewTokens: List by Delegates.notNull() + private var currentUserWallet: UserWallet by Delegates.notNull() + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + getNewCryptoCurrencies() + } else { + getLegacyCryptoCurrencies() + } + } + + private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies { + return when (val selectedWalletEither = getSelectedWalletSyncUseCase()) { + is Either.Left -> { + Timber.e(selectedWalletEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + currentUserWallet = selectedWalletEither.value + + when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { + is Either.Left -> { + Timber.e(currenciesEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + TokensListCryptoCurrencies( + coins = currenciesEither.value + .filterIsInstance() + .filterNot { it.isCustom } + .also { currentNewCoins = it } + .map { Blockchain.fromId(it.network.id.value) }, + tokens = currenciesEither.value + .filterIsInstance() + .filterNot(CryptoCurrency.Token::isCustom) + .also { currentNewTokens = it } + .map { token -> + TokenWithBlockchain( + token = Token( + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + id = token.id.rawCurrencyId, + ), + blockchain = Blockchain.fromId(token.network.id.value), + ) + }, + ) + } + } + } + } + } + + private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies { + val wallets = store.state.walletState.walletsDataFromStores + val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle() + + return TokensListCryptoCurrencies( + coins = wallets.toNonCustomBlockchains(derivationStyle), + tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle), + ) + } + + private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { + null + } else { + (walletDataModel.currency as? Currency.Blockchain)?.blockchain + } + } + .distinct() + } + + private fun List.toNonCustomTokensWithBlockchains( + derivationStyle: DerivationStyle?, + ): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency !is Currency.Token) return@mapNotNull null + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null + + TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) + } + .distinct() + } + + fun onSaveButtonClick( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList) + } else { + saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList) + } + } + + private fun saveByNewWay( + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + store.dispatch( + action = TokensAction.NewSaveChanges( + currentTokens = currentNewTokens, + currentCoins = currentNewCoins, + changedTokens = changedTokensList.mapNotNull { + cryptoCurrencyFactory.createToken( + sdkToken = it.token, + blockchain = it.blockchain, + extraDerivationPath = null, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + changedCoins = changedBlockchainList.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + userWallet = currentUserWallet, + ), + ) + } + + private fun saveByOldWay( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + val scanResponse = store.state.globalState.scanResponse ?: return + + store.dispatch( + action = TokensAction.LegacySaveChanges( + currentTokens = currentTokensList, + currentBlockchains = currentBlockchainList, + changedTokens = changedTokensList, + changedBlockchains = changedBlockchainList, + scanResponse = scanResponse, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 78ff6ff6ce..06d6c0ce2e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -1,15 +1,11 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.LoadState -import androidx.paging.PagingData -import androidx.paging.map +import androidx.lifecycle.* +import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.getActiveIconRes @@ -19,6 +15,10 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName @@ -26,49 +26,49 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.features.tokens.impl.domain.models.Token.Network import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState -import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensAction -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates import com.tangem.blockchain.common.Token as BlockchainToken /** * ViewModel for tokens list screen * - * @property interactor feature interactor - * @property router feature router - * @property dispatchers coroutine dispatchers provider - * @property reduxStateHolder redux state holder - * @param analyticsEventHandler analytics event handler + * @property interactor feature interactor + * @property router feature router + * @property dispatchers coroutine dispatchers provider + * @property getSelectedWalletSyncUseCase use case that returns selected wallet + * @param analyticsEventHandler analytics event handler * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel internal class TokensListViewModel @Inject constructor( private val interactor: TokensListInteractor, private val router: TokensListRouter, private val dispatchers: AppCoroutineDispatcherProvider, - private val reduxStateHolder: AppStateHolder, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, analyticsEventHandler: AnalyticsEventHandler, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, + walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { - private val args = TokensListArgs() + private val isManageAccess = store.state.tokensState.isManageAccess private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) @@ -76,15 +76,38 @@ internal class TokensListViewModel @Inject constructor( var uiState by mutableStateOf(value = getInitialUiState()) private set - private val changedTokensList: MutableList = args.mainScreenTokenList.toMutableList() - private val changedBlockchainList: MutableList = args.mainScreenBlockchainList.toMutableList() + private var currentTokensList: List by Delegates.notNull() + private var currentBlockchainList: List by Delegates.notNull() + + private var changedTokensList: MutableList = mutableListOf() + private var changedBlockchainList: MutableList = mutableListOf() + + private val tokensListMigration = TokensListMigration( + walletFeatureToggles = walletFeatureToggles, + getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + getCurrenciesUseCase = getCurrenciesUseCase, + ) + + init { + viewModelScope.launch(dispatchers.main) { + val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies() + + currentBlockchainList = currentCoins + currentTokensList = currentTokens + + changedBlockchainList = currentCoins.toMutableList() + changedTokensList = currentTokens.toMutableList() + + uiState = uiState.copySealed(tokens = getTokensListBySearchText(text = "")) + } + } override fun onCreate(owner: LifecycleOwner) { - if (args.isManageAccess) analyticsSender.sendWhenScreenOpened() + if (isManageAccess) analyticsSender.sendWhenScreenOpened() } private fun getInitialUiState(): TokensListStateHolder { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListStateHolder.ManageContent( toolbarState = getInitialToolbarState(), isLoading = true, @@ -105,7 +128,7 @@ internal class TokensListViewModel @Inject constructor( } private fun getInitialToolbarState(): TokensListToolbarState { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListToolbarState.Title.Manage( titleResId = R.string.add_tokens_title, onBackButtonClick = actionsHandler::onBackButtonClick, @@ -122,17 +145,19 @@ internal class TokensListViewModel @Inject constructor( } private fun isDifferentAddressesBlockVisible(): Boolean { - return reduxStateHolder.scanResponse?.card?.useOldStyleDerivation == true + return getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { it.scanResponse.card.useOldStyleDerivation }, + ) } - private fun getInitialTokensList(searchText: String = ""): Flow> { - if (searchText.isNotEmpty()) analyticsSender.sendWhenTokenSearched() - - return interactor.getTokensList(searchText = searchText).map { - it.map { token -> - if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) - } - } + private fun getInitialTokensList(): Flow> { + return flowOf( + value = PagingData.empty( + sourceLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading), + mediatorLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading), + ), + ) } private fun createManageTokenContent(token: Token): TokenItemState.ManageContent { @@ -152,9 +177,7 @@ internal class TokensListViewModel @Inject constructor( return NetworkItemState.ManageContent( name = network.blockchain.fullNameWithoutTestnet.uppercase(), protocolName = getNetworkProtocolName(network), - iconResId = mutableStateOf( - getNetworkIconResId(network.address, network.blockchain), - ), + iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)), isMainNetwork = isMainNetwork(network), isAdded = mutableStateOf( isAdded(address = network.address, blockchain = network.blockchain), @@ -183,7 +206,7 @@ internal class TokensListViewModel @Inject constructor( return NetworkItemState.ReadContent( name = network.blockchain.fullNameWithoutTestnet.uppercase(), protocolName = getNetworkProtocolName(network), - iconResId = mutableStateOf(getNetworkIconResId(network.address, network.blockchain)), + iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)), isMainNetwork = isMainNetwork(network), ) } @@ -218,6 +241,20 @@ internal class TokensListViewModel @Inject constructor( } } + private fun getTokensListBySearchText(text: String): Flow> { + return interactor.getTokensList(searchText = text) + .mapToTokenItemState() + .cachedIn(viewModelScope) + } + + private fun Flow>.mapToTokenItemState(): Flow> { + return map { pagingData -> + pagingData.map { token -> + if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) + } + } + } + private inner class ActionsHandler( private val router: TokensListRouter, private val debouncer: Debouncer, @@ -264,7 +301,12 @@ internal class TokensListViewModel @Inject constructor( fun onSaveButtonClick() { analyticsSender.sendWhenSaveButtonClicked() - store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList)) + tokensListMigration.onSaveButtonClick( + currentTokensList = currentTokensList, + currentBlockchainList = currentBlockchainList, + changedTokensList = changedTokensList, + changedBlockchainList = changedBlockchainList, + ) } private fun onSearchValueChange(newValue: String) { @@ -272,10 +314,15 @@ internal class TokensListViewModel @Inject constructor( uiState = uiState.copySealed(toolbarState = state.copy(value = newValue)) debouncer.debounce(waitMs = 800L, coroutineScope = viewModelScope + dispatchers.io) { - uiState = uiState.copySealed(tokens = getInitialTokensList(newValue)) + uiState = uiState.copySealed(tokens = getSearchedTokensList(newValue)) } } + private fun getSearchedTokensList(searchText: String): Flow> { + analyticsSender.sendWhenTokenSearched() + return getTokensListBySearchText(text = searchText) + } + private fun onCleanButtonClick() { val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField) @@ -291,7 +338,7 @@ internal class TokensListViewModel @Inject constructor( if (isRemoveAction) { val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } - val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain) + val isAddedOnMainScreen = currentBlockchainList.contains(blockchain) if (isTokenWithSameBlockchainFound) { router.openUnableHideMainTokenAlert( @@ -341,7 +388,7 @@ internal class TokensListViewModel @Inject constructor( val isRemoveAction = changedTokensList.contains(token) if (isRemoveAction) { - val isAddedOnMainScreen = args.mainScreenTokenList.contains(token) + val isAddedOnMainScreen = currentTokensList.contains(token) if (isAddedOnMainScreen) { router.openRemoveWalletAlert( @@ -373,32 +420,39 @@ internal class TokensListViewModel @Inject constructor( } private fun isUnsupportedToken(blockchain: Blockchain): SupportTokensState? { - val scanResponse = reduxStateHolder.scanResponse - val cardTypesResolver = scanResponse?.cardTypesResolver ?: return null - val supportedTokens = scanResponse.card.supportedTokens(cardTypesResolver) + return getSelectedWalletSyncUseCase().fold( + ifLeft = { null }, + ifRight = { + val cardTypesResolver = it.scanResponse.cardTypesResolver + val supportedTokens = it.scanResponse.card.supportedTokens(cardTypesResolver) - // refactor this later by moving all this logic in card config - if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { - return SupportTokensState.SolanaNetworkUnsupported - } - val canHandleToken = scanResponse.card.canHandleToken( - supportedTokens = supportedTokens, - blockchain = blockchain, - cardTypesResolver = cardTypesResolver, + // refactor this later by moving all this logic in card config + if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) { + return SupportTokensState.SolanaNetworkUnsupported + } + val canHandleToken = it.scanResponse.card.canHandleToken( + supportedTokens = supportedTokens, + blockchain = blockchain, + cardTypesResolver = cardTypesResolver, + ) + if (!canHandleToken) { + return SupportTokensState.UnsupportedCurve + } + return SupportTokensState.SupportedToken + }, ) - if (!canHandleToken) { - return SupportTokensState.UnsupportedCurve - } - return SupportTokensState.SupportedToken } private fun isUnsupportedBlockchain(blockchain: Blockchain): Boolean { - val scanResponse = reduxStateHolder.scanResponse - val canHandleToken = scanResponse?.card?.canHandleBlockchain( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - return !canHandleToken + return getSelectedWalletSyncUseCase().fold( + ifLeft = { false }, + ifRight = { + !it.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = it.scanResponse.cardTypesResolver, + ) + }, + ) } private companion object { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt deleted file mode 100644 index c24f982fca..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.tap.domain.model.WalletDataModel -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - - data class ManageAccess(val wallets: List, val derivationStyle: DerivationStyle?) : SetArgs - - object ReadAccess : SetArgs - } - - // TODO: [REDACTED_TASK_KEY] Remove this action - data class SaveChanges(val tokens: List, val blockchains: List) : TokensAction - - // TODO: Remove this action in 4.7 release - object PrepareAndNavigateToAddCustomToken : TokensAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 2785c63857..c108b2602f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -9,63 +9,105 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.DomainWrapped import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.domainStore +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* -import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber +@Suppress("LargeClass") object TokensMiddleware { val tokensMiddleware: Middleware = { _, _ -> { next -> { action -> when (action) { - is TokensAction.SaveChanges -> handleSaveChanges(action) - is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken() + is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action) + is TokensAction.NewSaveChanges -> handleNewSaveChanges(action) } next(action) } } } - private fun handleSaveChanges(action: TokensAction.SaveChanges) { + private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) { scope.launch { - val scanResponse = store.state.globalState.scanResponse ?: return@launch + val scanResponse = action.userWallet.scanResponse - val currentTokens = store.state.tokensState.addedTokens - val currentBlockchains = store.state.tokensState.addedBlockchains + val currentTokens = action.currentTokens + val currentBlockchains = action.currentCoins - val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = - store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains) + val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains) - val tokensToAdd = action.tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } } + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } } - removeCurrenciesIfNeeded( + removeNewCurrenciesIfNeeded( + userWalletId = action.userWallet.walletId, + currencies = blockchainsToRemove + tokensToRemove, + ) + + val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() + val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() + if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { + store.dispatchDebugErrorNotification(message = "Nothing to save") + store.dispatchOnMain(NavigationAction.PopBackTo()) + return@launch + } + + val currencyList = blockchainsToAdd + tokensToAdd + + if (scanResponse.supportsHdWallet()) { + deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { + submitNewAdd( + userWalletId = action.userWallet.walletId, + updatedScanResponse = it, + currencyList = currencyList, + ) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } else { + submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } + } + + private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) { + scope.launch { + val scanResponse = action.scanResponse + + val currentTokens = action.currentTokens + val currentBlockchains = action.currentBlockchains + + val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains) + + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = + currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } } + + removeLegacyCurrenciesIfNeeded( currencies = convertToCurrencies( blockchains = blockchainsToRemove, tokens = tokensToRemove, @@ -89,11 +131,11 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingBlockchains(scanResponse, currencyList) { - submitAdd(it, currencyList) + submitLegacyAdd(it, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } else { - submitAdd(scanResponse, currencyList) + submitLegacyAdd(scanResponse, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } @@ -104,15 +146,14 @@ object TokensMiddleware { tokens: List, derivationStyle: DerivationStyle?, ): List { - return blockchains.map { - Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) - } + tokens.map { - Currency.Token( - it.token, - it.blockchain, - it.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - } + return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } + + tokens.map { + Currency.Token( + token = it.token, + blockchain = it.blockchain, + derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath, + ) + } } private fun deriveMissingBlockchains( @@ -123,7 +164,7 @@ object TokensMiddleware { val config = CardConfig.createConfig(scanResponse.card) val derivationDataList = currencyList.mapNotNull { currency -> val curve = config.primaryCurve(currency.blockchain) - curve?.let { getDerivations(curve, scanResponse, currency) } + curve?.let { getLegacyDerivations(curve, scanResponse, currency) } } val derivations = buildMap> { derivationDataList.forEach { @@ -174,7 +215,69 @@ object TokensMiddleware { } } - private fun getDerivations(curve: EllipticCurve, scanResponse: ScanResponse, currency: Currency): DerivationData? { + private fun deriveMissingCoins( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { currency -> + val curve = config.primaryCurve(blockchain = Blockchain.fromId(currency.network.id.value)) + curve?.let { getNewDerivations(curve, scanResponse, currency) } + } + val derivations = buildMap> { + derivationDataList.forEach { + val current = this[it.derivations.first] + if (current != null) { + current.addAll(it.derivations.second) + current.distinct() + } else { + this[it.derivations.first] = it.derivations.second.toMutableList() + } + } + } + + if (derivations.isEmpty()) { + onSuccess(scanResponse) + return + } + + scope.launch { + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + ) + when (result) { + is CompletionResult.Success -> { + val newDerivedKeys = result.data.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) + + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) + delay(DELAY_SDK_DIALOG_CLOSE) + + onSuccess(updatedScanResponse) + } + is CompletionResult.Failure -> { + store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens")) + } + } + } + } + + private fun getLegacyDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: Currency, + ): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null val supportedCurves = currency.blockchain.getSupportedCurves() @@ -205,9 +308,45 @@ object TokensMiddleware { return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currency: CryptoCurrency, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val blockchain = Blockchain.fromId(currency.network.id.value) + val supportedCurves = blockchain.getSupportedCurves() + val path = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) + .takeIf { supportedCurves.contains(curve) } + + val customPath = currency.network.derivationPath.value?.let { + DerivationPath(it) + }.takeIf { supportedCurves.contains(curve) } + + val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + if (currency is CryptoCurrency.Coin && blockchain == Blockchain.Cardano) { + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + class DerivationData(val derivations: Pair>) - private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { + private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -228,7 +367,31 @@ object TokensMiddleware { } } - private suspend fun removeCurrenciesIfNeeded(currencies: List) { + private fun submitNewAdd( + userWalletId: UserWalletId, + updatedScanResponse: ScanResponse, + currencyList: List, + ) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository) + + scope.launch { + userWalletsListManager.update( + userWalletId = userWalletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = currencyList.map(CryptoCurrency::network).toSet(), + refresh = true, + ) + } + } + + private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List) { if (currencies.isEmpty()) return val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to remove currencies, no user wallet selected") @@ -237,55 +400,10 @@ object TokensMiddleware { walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } - private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { - return currency.derivationPath?.let { - !scanResponse.hasDerivation(currency.blockchain, it) - } ?: false - } + private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) return + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) - private fun handleAddingCustomToken() = scope.launch { - val onAddCustomToken = fun(customCurrency: CustomCurrency) { - val scanResponse = store.state.globalState.scanResponse ?: return - - fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List) { - submitAdd(scanResponse, currencyList) - // pop from the AddCustomTokenScreen - store.dispatchOnMain(NavigationAction.PopBackTo()) - store.dispatchOnMain(NavigationAction.PopBackTo()) - } - - Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency)) - val currency = Currency.fromCustomCurrency(customCurrency) - val isNeedToDerive = isNeedToDerive(scanResponse, currency) - val currencyList = listOf(currency) - if (isNeedToDerive) { - deriveMissingBlockchains(scanResponse, currencyList) { - submitAndPopBack(it, currencyList) - } - } else { - submitAndPopBack(scanResponse, currencyList) - } - } - - val addedCurrencies = store.state.walletState.walletsStores - .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } - .flatten() - .map { currency -> - when (currency) { - is Currency.Blockchain -> DomainWrapped.Currency.Blockchain( - currency.blockchain, - currency.derivationPath, - ) - - is Currency.Token -> DomainWrapped.Currency.Token( - currency.token, - currency.blockchain, - currency.derivationPath, - ) - } - } - domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) - domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt index 8e33fdc8f4..389a02219a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.Currency.Token import org.rekotlin.Action object TokensReducer { @@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState { if (action !is TokensAction) return state.tokensState return when (action) { - is TokensAction.SetArgs.ManageAccess -> { - state.tokensState.copy( - isManageAccess = true, - addedWallets = action.wallets, - addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle), - addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle), - ) - } - - is TokensAction.SetArgs.ReadAccess -> { - state.tokensState.copy(isManageAccess = false) - } - + is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true) + is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false) else -> state.tokensState } -} - -private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { - null - } else { - (walletDataModel.currency as? Currency.Blockchain)?.blockchain - } - }.distinct() -} - -private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, -): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency !is Token) return@mapNotNull null - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - - TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) - }.distinct() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt index 156fd0c922..14c5d9d721 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt @@ -1,16 +1,5 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.tap.domain.model.WalletDataModel import org.rekotlin.StateType -data class TokensState( - val isManageAccess: Boolean = false, - val addedWallets: List = emptyList(), - val addedTokens: List = emptyList(), - val addedBlockchains: List = emptyList(), -) : StateType - -// TODO: [REDACTED_TASK_KEY] Remove this class -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file +data class TokensState(val isManageAccess: Boolean = false) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 8b22dd95e6..891841469c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -1,13 +1,15 @@ package com.tangem.tap.features.wallet.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : TwoWayConverter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } @@ -16,6 +18,7 @@ internal class CryptoCurrencyConverter : Converter { is Currency.Blockchain -> requireNotNull( cryptoCurrencyFactory.createCoin( blockchain = value.blockchain, + extraDerivationPath = value.derivationPath, derivationStyleProvider = requireNotNull( store.state.globalState .userWalletsListManager @@ -29,6 +32,7 @@ internal class CryptoCurrencyConverter : Converter { cryptoCurrencyFactory.createToken( sdkToken = value.token, blockchain = value.blockchain, + extraDerivationPath = value.derivationPath, derivationStyleProvider = requireNotNull( store.state.globalState .userWalletsListManager @@ -40,4 +44,26 @@ internal class CryptoCurrencyConverter : Converter { ) } } + + override fun convertBack(value: CryptoCurrency): Currency { + val blockchain = Blockchain.fromId(value.network.id.value) + if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") + return when (value) { + is CryptoCurrency.Coin -> Currency.Blockchain( + blockchain = blockchain, + derivationPath = value.network.derivationPath.value, + ) + is CryptoCurrency.Token -> Currency.Token( + token = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.value, + ), + blockchain = blockchain, + derivationPath = value.network.derivationPath.value, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt index e610e03389..e0b6ce6972 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/data/WalletRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.data +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.tap.features.wallet.domain.WalletRepository @@ -18,6 +19,6 @@ class WalletRepositoryImpl( ) : WalletRepository { override suspend fun getCurrencyList(): CurrenciesResponse = withContext(dispatchers.io) { - tangemTechApi.getCurrencyList() + tangemTechApi.getCurrencyList().getOrThrow() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index adca4f96f4..30b0aaadce 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -19,6 +19,7 @@ import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.wallet.R +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class WalletAction : Action { @@ -76,7 +77,7 @@ sealed class WalletAction : Action { data class Scan( val onScanSuccessEvent: AnalyticsEvent?, - val lifecycleScope: LifecycleCoroutineScope, + val scope: CoroutineScope, ) : WalletAction() data class Send(val amount: Amount? = null) : WalletAction() @@ -113,7 +114,7 @@ sealed class WalletAction : Action { data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() object CreateWallet : WalletAction() - data class ChangeWallet(val lifecycleScope: LifecycleCoroutineScope) : WalletAction() + data class ChangeWallet(val scope: LifecycleCoroutineScope) : WalletAction() object ShowSaveWalletIfNeeded : WalletAction() sealed class TradeCryptoAction : WalletAction() { @@ -121,14 +122,6 @@ sealed class WalletAction : Action { data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction() - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - data class SendCrypto( - val currencyId: String, - val amount: String, - val destinationAddress: String, - val transactionId: String, - ) : TradeCryptoAction() - object Swap : TradeCryptoAction() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 91fa88858c..1b3426fda4 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.wallet.redux.middlewares -import androidx.core.os.bundleOf import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard import com.tangem.common.flatMap @@ -8,7 +7,6 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken import com.tangem.tap.common.extensions.addContext @@ -17,7 +15,6 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog @@ -32,18 +29,12 @@ import timber.log.Timber class MultiWalletMiddleware { - private val cryptoCurrencyConverter by lazy { CryptoCurrencyConverter() } - @Suppress("LongMethod", "ComplexMethod") fun handle(action: WalletAction.MultiWallet, walletState: WalletState?) { when (action) { is WalletAction.MultiWallet.SelectWallet -> { if (action.currency != null) { - val bundle = bundleOf( - // TODO: [REDACTED_JIRA] - TokenDetailsRouter.SELECTED_CURRENCY_KEY to cryptoCurrencyConverter.convert(action.currency), - ) - store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails, bundle = bundle)) + store.dispatch(NavigationAction.NavigateTo(screen = AppScreen.WalletDetails)) } } is WalletAction.MultiWallet.TryToRemoveWallet -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2528753b4e..666f1ac84e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -11,16 +11,20 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.features.send.navigation.SendRouter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.TapError import com.tangem.tap.domain.tokens.getIconUrl import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE @@ -29,6 +33,7 @@ import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens import com.tangem.tap.proxy.redux.DaggerGraphState @@ -39,7 +44,10 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency +@Suppress("LargeClass") class TradeCryptoMiddleware { + + @Suppress("LongMethod", "CyclomaticComplexMethod") fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return @@ -49,14 +57,20 @@ class TradeCryptoMiddleware { is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action) is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Swap -> { - openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) + openSwap( + currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency(), + derivationPath = store.state.walletState.selectedWalletData?.currency?.derivationPath, + ) } is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) - TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send()) is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) - is TradeCryptoAction.New.Swap -> { - openSwap(currency = action.cryptoCurrency.toSwapCurrency()) - } + is TradeCryptoAction.New.Swap -> openSwap( + currency = action.cryptoCurrency.toSwapCurrency(), + derivationPath = action.cryptoCurrency.network.derivationPath.value, + network = action.cryptoCurrency.network, + ) + is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) + is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) } } @@ -100,6 +114,7 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, walletAddress = addresses[0].address, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, )?.let { store.dispatchOpenUrl(it) Analytics.send(Token.Topup.ScreenOpened()) @@ -109,24 +124,41 @@ class TradeCryptoMiddleware { private fun proceedNewBuyAction(state: () -> AppState?, action: TradeCryptoAction.New.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress?.defaultAddress ?: return - if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { - store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog()) - return - } - val status = action.cryptoCurrencyStatus val currency = status.currency val blockchain = Blockchain.fromId(currency.network.id.value) + val exchangeManager = store.state.globalState.exchangeManager + val topUrl = exchangeManager.getUrl( + action = CurrencyExchangeManager.Action.Buy, + blockchain = blockchain, + cryptoCurrencyName = currency.symbol, + fiatCurrencyName = action.appCurrencyCode, + walletAddress = networkAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + ) + + if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) { + val dialogData = topUrl?.let { + WalletDialog.RussianCardholdersWarningDialog.Data( + topUpUrl = it, + ) + } + store.dispatchOnMain( + WalletAction.DialogAction.RussianCardholdersWarningDialog( + dialogData = dialogData, + ), + ) + return + } + if (currency is CryptoCurrency.Token && currency.network.isTestnet) { scope.launch { val walletManager = store.state.daggerGraphState .get(DaggerGraphState::walletManagersFacade) .getOrCreateWalletManager( - userWallet = action.userWallet, + userWalletId = action.userWallet.walletId, blockchain = blockchain, - derivationPath = blockchain.derivationPath( - style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - ), + derivationPath = currency.network.derivationPath.value, ) if (walletManager !is EthereumWalletManager) { @@ -143,14 +175,7 @@ class TradeCryptoMiddleware { return } - val exchangeManager = store.state.globalState.exchangeManager - exchangeManager.getUrl( - action = CurrencyExchangeManager.Action.Buy, - blockchain = blockchain, - cryptoCurrencyName = currency.symbol, - fiatCurrencyName = action.appCurrencyCode, - walletAddress = networkAddress, - )?.let { + topUrl?.let { store.dispatchOpenUrl(it) Analytics.send(Token.Topup.ScreenOpened()) } @@ -172,6 +197,7 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, walletAddress = addresses[0].address, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, )?.let { store.dispatchOpenUrl(it) Analytics.send(Token.Withdraw.ScreenOpened()) @@ -188,6 +214,7 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.symbol, fiatCurrencyName = action.appCurrencyCode, walletAddress = networkAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, )?.let { store.dispatchOpenUrl(it) Analytics.send(Token.Withdraw.ScreenOpened()) @@ -239,10 +266,11 @@ class TradeCryptoMiddleware { )?.let { store.dispatchOpenUrl(it) } } - private fun openSwap(currency: SwapCurrency?) { + private fun openSwap(currency: SwapCurrency?, derivationPath: String?, network: Network? = null) { val bundle = bundleOf( SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency), - SwapFragment.DERIVATION_PATH to store.state.walletState.selectedWalletData?.currency?.derivationPath, + SwapFragment.DERIVATION_PATH to derivationPath, + SwapFragment.NETWORK to network, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) @@ -265,11 +293,11 @@ class TradeCryptoMiddleware { } is CryptoCurrency.Token -> { SwapCurrency.NonNativeToken( - id = id.value, + id = id.rawCurrencyId ?: "", name = name, symbol = symbol, networkId = blockchain.toNetworkId(), - logoUrl = getIconUrl(id.value), + logoUrl = getIconUrl(id.rawCurrencyId ?: ""), contractAddress = contractAddress, decimalCount = decimals, ) @@ -301,4 +329,95 @@ class TradeCryptoMiddleware { ) } } + + private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + val cryptoStatus = action.tokenStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWalletId = action.userWallet.walletId, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token } + when (currency) { + is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin") + is CryptoCurrency.Token -> { + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = walletManager.wallet.amounts[AmountType.Coin], + coinRate = action.coinFiatRate, + tokenAmount = sendableAmounts.first(), + tokenRate = cryptoStatus.value.fiatRate, + ), + ) + } + } + val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + } + } + + private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + val cryptoStatus = action.coinStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWalletId = action.userWallet.walletId, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin } + when (currency) { + is CryptoCurrency.Coin -> { + val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol } + + if (amountToSend == null) { + val error = TapError.UnsupportedState(stateError = "Amount to send is null") + FirebaseCrashlytics.getInstance() + .recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = amountToSend, + coinRate = cryptoStatus.value.fiatRate, + ), + ) + } + is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") + } + + val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index b4f81bd001..ce93801220 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -116,9 +116,9 @@ class WalletMiddleware { } is WalletAction.Scan -> { store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) - action.lifecycleScope.launch { + action.scope.launch { delay(timeMillis = 700) - store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent, action.lifecycleScope)) + store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent, action.scope)) } } is WalletAction.LoadData, @@ -227,7 +227,7 @@ class WalletMiddleware { showSaveWalletIfNeeded() } is WalletAction.ChangeWallet -> { - changeWallet(walletState, action.lifecycleScope) + changeWallet(walletState, action.scope) } is WalletAction.UserWalletChanged -> Unit is WalletAction.WalletStoresChanged -> { @@ -403,7 +403,7 @@ class WalletMiddleware { store.dispatch( WalletAction.Scan( onScanSuccessEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main), - lifecycleScope = lifecycleScope, + scope = lifecycleScope, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 44a7dd5341..39e905255e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -19,11 +19,12 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WarningsMiddleware { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { when (action) { @@ -49,7 +50,7 @@ class WarningsMiddleware { if (action.remainingSignatures != null && action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { - store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format) + // store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format) addWarningMessage( warning = WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures), autoUpdate = true, @@ -154,9 +155,8 @@ class WarningsMiddleware { store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) } is SimpleResult.Failure -> - if (result.error is BlockchainSdkError.SignatureCountNotMatched) { - addWarningMessage(alreadySignedHashesWarning, true) - } else if (signedHashes > 0) { + if (signedHashes > 0 || result.error is BlockchainSdkError.SignatureCountNotMatched) { + alreadySignedHashesWarning.isHidden = false addWarningMessage(alreadySignedHashesWarning, true) } null -> Unit diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 3cab6df274..eb7dfd7126 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -10,6 +10,8 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.CardBalanceBinding +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class BalanceWidget( private val binding: CardBalanceBinding, private val fragment: WalletFragment, @@ -46,12 +48,11 @@ class BalanceWidget( val statusView = if (blockchainWalletData.status is WalletDataModel.VerifiedOnline) { R.id.tv_status_verified } else { - tvStatusError.text = - fragment.getText(R.string.wallet_balance_tx_in_progress) + // tvStatusError.text = fragment.getText(R.string.wallet_balance_tx_in_progress) R.id.group_error } showStatus(statusView) - tvStatusErrorMessage.hide() + // tvStatusErrorMessage.hide() if (tokenWalletData != null) { showBalanceWithToken(blockchainWalletData, true) @@ -70,12 +71,12 @@ class BalanceWidget( tvCurrency.text = currency tvAmount.text = "" - tvStatusErrorMessage.text = blockchainWalletData.status.errorMessage - tvStatusError.text = - fragment.getString(R.string.wallet_balance_blockchain_unreachable) + // tvStatusErrorMessage.text = blockchainWalletData.status.errorMessage + // TODO: Delete with WalletFeatureToggles + // tvStatusError.text = fragment.getString(R.string.wallet_balance_blockchain_unreachable) showStatus(R.id.group_error) - tvStatusErrorMessage.show(!blockchainWalletData.status.errorMessage.isNullOrBlank()) + // tvStatusErrorMessage.show(!blockchainWalletData.status.errorMessage.isNullOrBlank()) } is WalletDataModel.NoAccount -> with(binding.lBalanceError) { binding.lBalance.root.hide() @@ -93,7 +94,7 @@ class BalanceWidget( } private fun showStatus(@IdRes viewRes: Int) = with(binding.lBalance) { - groupError.show(viewRes == R.id.group_error) + // groupError.show(viewRes == R.id.group_error) tvStatusLoading.show(viewRes == R.id.tv_status_loading) tvStatusVerified.show(viewRes == R.id.tv_status_verified) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index fe767bafda..de08710457 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -10,7 +10,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager -import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding import com.badoo.mvicore.DiffStrategy import com.badoo.mvicore.ModelWatcher @@ -63,7 +62,9 @@ import javax.inject.Inject /** * Wallet details fragment - use only for MultiWallet */ +// TODO: Delete with WalletFeatureToggles @Suppress("LargeClass", "MagicNumber") +@Deprecated(message = "Used only in old wallet screen") @AndroidEntryPoint class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeStoreSubscriber { @@ -152,9 +153,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt } }, ) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.slide_right) - exitTransition = inflater.inflateTransition(R.transition.fade) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -440,10 +438,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.hide() lBalance.tvError.show() - lBalance.tvError.setWarningStatus( - R.string.wallet_balance_blockchain_unreachable, - status.errorMessage, - ) + // TODO: Delete with WalletFeatureToggles + // lBalance.tvError.setWarningStatus( + // R.string.wallet_balance_blockchain_unreachable, + // status.errorMessage, + // ) } is WalletDataModel.NoAccount -> { lBalance.root.hide() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index cbb33a7dcd..a02a218d62 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -37,7 +37,6 @@ import com.tangem.tap.common.utils.SafeStoreSubscriber import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printWalletState -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -73,7 +72,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber() private val viewModel by viewModels() private val totalBalanceWatcher = modelWatcher { @@ -101,9 +99,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) - store.state.globalState.scanResponse?.let { scanResponse -> - store.dispatch( - DetailsAction.PrepareScreen( - scanResponse = scanResponse, - wallets = store.state.walletState.walletManagers.map { it.wallet }, - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) - true - } - false + store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) + + true } else -> super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt index b94cc9de26..271bc8aaae 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt @@ -64,6 +64,7 @@ internal class WalletViewModel @Inject constructor( batch = scanResponse.card.batchId, signInType = signInType, walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt index afbf3bff5a..5f98692fca 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt @@ -9,43 +9,45 @@ import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WalletWarningConverter( private val context: Context, ) : ModuleMessageConverter { override fun convert(message: WalletWarning): WalletWarningDescription { - val warningMessage = when (message) { - is WalletWarning.ExistentialDeposit -> { - context.getString( - R.string.warning_existential_deposit_message, - message.currencyName, - message.edStringValueWithSymbol, - ) - } - is WalletWarning.BalanceNotEnoughForFee -> { - context.getString( - R.string.token_details_send_blocked_fee_format, - message.currencyName, - message.blockchainFullName, - message.currencyName, - message.blockchainFullName, - message.blockchainSymbol, - ) - } - is WalletWarning.TransactionInProgress -> { - context.getString( - R.string.token_details_send_blocked_tx_format, - message.currencyName, - ) - } - is WalletWarning.Rent -> { - context.getString( - R.string.solana_rent_warning, - message.walletRent.rent, - message.walletRent.exemptionAmount, - ) - } - } - return WalletWarningDescription(context.getString(R.string.common_warning), warningMessage) + // val warningMessage = when (message) { + // is WalletWarning.ExistentialDeposit -> { + // context.getString( + // R.string.warning_existential_deposit_message, + // message.currencyName, + // message.edStringValueWithSymbol, + // ) + // } + // is WalletWarning.BalanceNotEnoughForFee -> { + // context.getString( + // R.string.token_details_send_blocked_fee_format, + // message.currencyName, + // message.blockchainFullName, + // message.currencyName, + // message.blockchainFullName, + // message.blockchainSymbol, + // ) + // } + // is WalletWarning.TransactionInProgress -> { + // context.getString( + // R.string.token_details_send_blocked_tx_format, + // message.currencyName, + // ) + // } + // is WalletWarning.Rent -> { + // context.getString( + // R.string.solana_rent_warning, + // message.walletRent.rent, + // message.walletRent.exemptionAmount, + // ) + // } + // } + return WalletWarningDescription(context.getString(R.string.common_warning), "") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 3f511f7b65..0b1e671dab 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -22,6 +22,8 @@ import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.ItemCurrencyWalletBinding +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") class WalletAdapter : ListAdapter(DiffUtilCallback) { override fun getItemId(position: Int): Long { @@ -59,7 +61,8 @@ class WalletAdapter : ListAdapter { - root.getString(R.string.wallet_balance_blockchain_unreachable) + // TODO: Delete with WalletFeatureToggles + // root.getString(R.string.wallet_balance_blockchain_unreachable) } is WalletDataModel.MissedDerivation -> { root.getString(R.string.wallet_balance_missing_derivation) @@ -86,7 +89,7 @@ class WalletAdapter : ListAdapter(DiffUtilCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH { @@ -94,11 +91,11 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi val buttonAction = when (warning.titleResId) { - R.string.warning_important_security_info -> { - View.OnClickListener { - store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog) - } - } + // R.string.warning_important_security_info -> { + // View.OnClickListener { + // store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog) + // } + // } else -> { View.OnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) @@ -120,12 +117,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(WalletAction.Warnings.AppRating.RemindLater) } - binding.btnCanBeBetter.setOnClickListener { - Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) - store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) - store.dispatch(GlobalAction.HideWarningMessage(warning)) - store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) - } + // binding.btnCanBeBetter.setOnClickListener { + // Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) + // store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) + // store.dispatch(GlobalAction.HideWarningMessage(warning)) + // store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) + // } binding.btnReallyCool.setOnClickListener { val activity = binding.root.context.getActivity() ?: return@setOnClickListener diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index f1081006c2..30d7d0b8e4 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.dispatchDialogHide @@ -15,7 +16,7 @@ import com.tangem.wallet.R */ object ScanFailsDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(R.string.common_warning)) setMessage(R.string.alert_troubleshooting_scan_card_title) setPositiveButton(R.string.alert_button_request_support) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt index c44801d9c8..8ebbffeb54 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt @@ -2,17 +2,20 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R +// TODO: Delete with WalletFeatureToggles +@Deprecated(message = "Used only in old wallet screen") object SignedHashesWarningDialog { fun create(context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { - setTitle(context.getString(R.string.warning_important_security_info, "\u26A0")) - setMessage(R.string.alert_signed_hashes_message) + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { + // setTitle(context.getString(R.string.warning_important_security_info, "\u26A0")) + // setMessage(R.string.alert_signed_hashes_message) setPositiveButton(R.string.common_understand) { _, _ -> store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) store.dispatch( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt index 8f3a42060f..9abf465eb0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SimpleOkDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.store @@ -13,7 +14,7 @@ import com.tangem.wallet.R object SimpleOkDialog { fun create(dialog: AppDialog.SimpleOkDialog, context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(dialog.header) setMessage(dialog.message) setPositiveButton(R.string.common_ok) { _, _ -> } @@ -60,7 +61,7 @@ object SimpleOkDialog { ) fun create(dialog: AppDialog.OkCancelDialogRes, context: Context): AlertDialog { - return AlertDialog.Builder(context).apply { + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(context.getString(dialog.headerId)) setMessage(dialog.messageId) setPositiveButton(dialog.okButton.title) { _, _ -> dialog.okButton.action?.invoke() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt index fdfadc43e1..d39bf43e05 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/view/TotalBalanceCard.kt @@ -134,7 +134,7 @@ private fun TotalBalanceCardContent(state: TotalBalanceCardState, modifier: Modi Text( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.main_processing_full_amount), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.attention, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt index 835b547e6d..51a29dfdba 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/CurrencySelectionDialog.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.wallet import android.content.Context import androidx.appcompat.app.AlertDialog +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store @@ -15,7 +16,7 @@ object CurrencySelectionDialog { val currentSelection = dialog.currenciesList .indexOfFirst { it.code == dialog.currentAppCurrency.code } - return AlertDialog.Builder(context) + return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) .setTitle(context.getString(R.string.details_row_title_currency)) .setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> /* no-op */ } .setOnDismissListener { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 8f17e52682..290bd68881 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.entities.FiatCurrency @@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.TotalFiatBalance -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -106,14 +105,8 @@ class MultiWalletView : WalletView() { binding.btnAddToken.setOnClickListener { Analytics.send(Portfolio.ButtonManageTokens()) - store.dispatch( - TokensAction.SetArgs.ManageAccess( - wallets = state.walletsDataFromStores, - derivationStyle = store.state.globalState.scanResponse - ?.derivationStyleProvider?.getDerivationStyle(), - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + store.dispatch(action = TokensAction.SetArgs.ManageAccess) + store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 3ee7ad011b..1593e811f9 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -288,6 +288,9 @@ internal class WalletSelectorMiddleware { } private fun refreshUserWalletsAmounts() { + val featureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (featureToggles.isRedesignedScreenEnabled) return + scope.launch { walletStoresManager.updateAmounts( userWallets = userWalletsListManager.userWallets.firstOrNull().orEmpty(), diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt index 472a4322ec..0993a1c9ea 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt @@ -177,7 +177,7 @@ private fun Footer( PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource( - id = R.string.user_wallet_list_unlock_all, + id = R.string.user_wallet_list_unlock_all_with, stringResource(id = R.string.common_biometrics), ), showProgress = showUnlockProgress, diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index 62ef2c0468..0224ee04fc 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -151,7 +151,7 @@ private fun RowScope.WalletInfo(wallet: UserWalletItem, isSelected: Boolean) { }, color = TangemTheme.colors.text.tertiary, maxLines = 1, - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, ) } } @@ -255,7 +255,7 @@ private fun LoadedTokensInfo( count = tokensCount, tokensCount, ), - style = TangemTheme.typography.caption, + style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.End, ) diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt index 338743a569..97138375c6 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt @@ -1,22 +1,27 @@ package com.tangem.tap.features.welcome.redux import android.content.Intent -import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.core.TangemError +import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action internal sealed interface WelcomeAction : Action { - object ProceedWithBiometrics : WelcomeAction { + + data class SetCoroutineScope(val scope: CoroutineScope) : WelcomeAction + + object ClearCoroutineScope : WelcomeAction + + data class ProceedWithBiometrics(val afterUnlockIntent: Intent? = null) : WelcomeAction { object Success : WelcomeAction data class Error(val error: TangemError) : WelcomeAction } - data class ProceedWithCard(val lifecycleCoroutineScope: LifecycleCoroutineScope) : WelcomeAction { + object ProceedWithCard : WelcomeAction { object Success : WelcomeAction data class Error(val error: TangemError) : WelcomeAction } - data class SetInitialIntent(val intent: Intent?) : WelcomeAction + data class ProceedWithIntent(val intent: Intent) : WelcomeAction object CloseError : WelcomeAction diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 1ab1cee43d..482671ce51 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -1,26 +1,31 @@ package com.tangem.tap.features.welcome.redux -import androidx.lifecycle.LifecycleCoroutineScope +import android.content.Intent import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.flatMap +import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* +import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.signin.redux.SignInAction import com.tangem.tap.proxy.redux.DaggerGraphState +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -39,15 +44,119 @@ internal class WelcomeMiddleware { } private fun handleAction(action: WelcomeAction, state: WelcomeState) { - when (action) { - is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(state) - is WelcomeAction.ProceedWithCard -> proceedWithCard(state, action.lifecycleCoroutineScope) - is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() - else -> Unit + state.scope?.launch { + when (action) { + is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this) + is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics( + afterUnlockIntent = action.afterUnlockIntent ?: state.intent, + ) + is WelcomeAction.ProceedWithCard -> proceedWithCard(afterScanIntent = state.intent) + is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() + else -> Unit + } } } - private fun disableUserWalletsSaving() = scope.launch { + private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) { + Timber.d( + """ + Proceeding with intent + |- Intent: $initialIntent + """.trimIndent(), + ) + + val handler = BackgroundScanIntentHandler( + scope = scope, + hasSavedUserWalletsProvider = { true }, + ) + val isBackgroundScanHandled = handler.handleIntent(initialIntent) + val hasUncompletedBackup = backupService.hasIncompletedBackup + + if (!isBackgroundScanHandled && !hasUncompletedBackup) { + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent)) + } + } + + private suspend fun proceedWithBiometrics(afterUnlockIntent: Intent?) { + Timber.d( + """ + Proceeding with biometry + |- Intent: $afterUnlockIntent + """.trimIndent(), + ) + + userWalletsListManager.unlockIfLockable() + .doOnFailure { error -> + Timber.e(error, "Unable to unlock user wallets with biometrics") + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) + } + .doOnSuccess { selectedUserWallet -> + sendSignedInAnalyticsEvent( + scanResponse = selectedUserWallet.scanResponse, + signInType = Basic.SignedIn.SignInType.Biometric, + ) + + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric)) + store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) + store.onUserWalletSelected(userWallet = selectedUserWallet) + + afterUnlockIntent?.let { + WalletConnectLinkIntentHandler().handleIntent(it) + } + } + } + + private suspend fun proceedWithCard(afterScanIntent: Intent?) { + Timber.d( + """ + Proceeding with card + |- Intent: $afterScanIntent + """.trimIndent(), + ) + + scanCardInternal { scanResponse -> + val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal + + userWalletsListManager.save(userWallet, canOverride = true) + .doOnFailure { error -> + Timber.e(error, "Unable to save user wallet") + store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) + } + .doOnSuccess { + sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card) + + store.dispatchWithMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) + store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) + store.onUserWalletSelected(userWallet = userWallet) + + afterScanIntent?.let { + WalletConnectLinkIntentHandler().handleIntent(it) + } + } + } + } + + private fun sendSignedInAnalyticsEvent(scanResponse: ScanResponse, signInType: Basic.SignedIn.SignInType) { + val currency = ParamCardCurrencyConverter().convert( + value = scanResponse.cardTypesResolver, + ) + + if (currency != null) { + Analytics.send( + event = Basic.SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = signInType, + walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private suspend fun disableUserWalletsSaving() { userWalletsListManager.clear() .flatMap { walletStoresManager.clear() } .flatMap { tangemSdkManager.clearSavedUserCodes() } @@ -60,48 +169,6 @@ internal class WelcomeMiddleware { } } - private fun proceedWithBiometrics(state: WelcomeState) = scope.launch { - userWalletsListManager.unlockIfLockable() - .doOnFailure { error -> - Timber.e(error, "Unable to unlock user wallets with biometrics") - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Error(error)) - } - .doOnSuccess { selectedUserWallet -> - store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Biometric)) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) - store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) - store.onUserWalletSelected(userWallet = selectedUserWallet) - - state.intent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) - } - } - } - - private fun proceedWithCard(state: WelcomeState, lifecycleCoroutineScope: LifecycleCoroutineScope) { - lifecycleCoroutineScope.launch { - scanCardInternal { scanResponse -> - val userWallet = UserWalletBuilder(scanResponse).build() ?: return@scanCardInternal - - userWalletsListManager.save(userWallet, canOverride = true) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) - } - .doOnSuccess { - store.dispatchOnMain(SignInAction.SetSignInType(Basic.SignedIn.SignInType.Card)) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) - store.onUserWalletSelected(userWallet = userWallet) - - state.intent?.let { - WalletConnectLinkIntentHandler().handleIntent(it) - } - } - } - } - } - private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt index 11bfb0ea4a..85db06dc63 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -14,7 +14,9 @@ internal object WelcomeReducer { private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { return when (action) { - is WelcomeAction.SetInitialIntent -> state.copy(intent = action.intent) + is WelcomeAction.SetCoroutineScope -> state.copy(scope = action.scope) + is WelcomeAction.ClearCoroutineScope -> state.copy(scope = null) + is WelcomeAction.ProceedWithIntent -> state.copy(intent = action.intent) is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt index a8a55d3984..951fa1e673 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -2,9 +2,11 @@ package com.tangem.tap.features.welcome.redux import android.content.Intent import com.tangem.common.core.TangemError +import kotlinx.coroutines.CoroutineScope import org.rekotlin.StateType data class WelcomeState( + val scope: CoroutineScope? = null, val isUnlockWithBiometricsInProgress: Boolean = false, val isUnlockWithCardInProgress: Boolean = false, val intent: Intent? = null, diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index f3d9cd27f1..c3197818c0 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -11,9 +11,9 @@ import androidx.compose.material.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.tangem.core.analytics.Analytics import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -33,8 +33,6 @@ internal class WelcomeFragment : ComposeFragment() { @Inject override lateinit var appThemeModeHolder: AppThemeModeHolder - private val viewModel by viewModels() - override fun onStart() { super.onStart() Analytics.eraseContext() @@ -43,6 +41,9 @@ internal class WelcomeFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { + val viewModel = hiltViewModel() + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + val state by viewModel.state.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) @@ -66,7 +67,7 @@ internal class WelcomeFragment : ComposeFragment() { showUnlockProgress = state.showUnlockWithBiometricsProgress, showScanCardProgress = state.showUnlockWithCardProgress, onUnlockClick = viewModel::unlockWallets, - onScanCardClick = { viewModel.scanCard(lifecycleCoroutineScope = lifecycleScope) }, + onScanCardClick = viewModel::scanCard, ) SnackbarHost( @@ -87,4 +88,8 @@ internal class WelcomeFragment : ComposeFragment() { } } } + + internal companion object { + const val INITIAL_INTENT_KEY = "intent" + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index 4f7587289d..fbd107685e 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.welcome.ui -import androidx.lifecycle.LifecycleCoroutineScope -import androidx.lifecycle.ViewModel +import android.content.Intent +import androidx.lifecycle.* import com.tangem.common.core.TangemError import com.tangem.core.analytics.Analytics import com.tangem.domain.wallets.legacy.UserWalletsListError @@ -12,28 +12,50 @@ import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.welcome.ui.model.WarningModel import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@HiltViewModel +internal class WelcomeViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, +) : ViewModel(), + StoreSubscriber, + DefaultLifecycleObserver { + + private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY] -internal class WelcomeViewModel : ViewModel(), StoreSubscriber { private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal init { + store.dispatch(WelcomeAction.SetCoroutineScope(viewModelScope)) + subscribeToStoreChanges() initGlobalState() } - fun unlockWallets() { - Analytics.send(SignIn.ButtonBiometricSignIn()) - store.dispatch(WelcomeAction.ProceedWithBiometrics) + override fun onCreate(owner: LifecycleOwner) { + val welcomeAction = if (initialIntent != null) { + WelcomeAction.ProceedWithIntent(initialIntent) + } else { + WelcomeAction.ProceedWithBiometrics() + } + + store.dispatch(welcomeAction) } - fun scanCard(lifecycleCoroutineScope: LifecycleCoroutineScope) { + fun unlockWallets() { + Analytics.send(SignIn.ButtonBiometricSignIn()) + store.dispatch(WelcomeAction.ProceedWithBiometrics()) + } + + fun scanCard() { Analytics.send(SignIn.ButtonCardSignIn()) - store.dispatch(WelcomeAction.ProceedWithCard(lifecycleCoroutineScope)) + store.dispatch(WelcomeAction.ProceedWithCard) } fun closeError() { @@ -60,6 +82,7 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber { } override fun onCleared() { + store.dispatch(WelcomeAction.ClearCoroutineScope) store.unsubscribe(this) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt index 07804272ed..f9d2d16492 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt @@ -42,8 +42,16 @@ internal class BuyExchangeService( cryptoCurrencyName: String, fiatCurrencyName: String, walletAddress: String, + isDarkTheme: Boolean, ): String? { - return currentService.getUrl(action, blockchain, cryptoCurrencyName, fiatCurrencyName, walletAddress) + return currentService.getUrl( + action, + blockchain, + cryptoCurrencyName, + fiatCurrencyName, + walletAddress, + isDarkTheme, + ) } override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index ad03f2c04a..037b139a49 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -50,6 +50,7 @@ class CurrencyExchangeManager( cryptoCurrencyName: CryptoCurrencyName, fiatCurrencyName: String, walletAddress: String, + isDarkTheme: Boolean, ): String? { if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl() @@ -60,6 +61,7 @@ class CurrencyExchangeManager( cryptoCurrencyName, fiatCurrencyName, walletAddress, + isDarkTheme, ) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt new file mode 100644 index 0000000000..b8a3e32d55 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter + +class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { + + private val cryptoCurrencyConverter = CryptoCurrencyConverter() + override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForBuy( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } + + override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForSell( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index f38a8c64a1..442a0a6db0 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -28,6 +28,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { cryptoCurrencyName: String, fiatCurrencyName: String, walletAddress: String, + isDarkTheme: Boolean, ): String? = null override fun getSellCryptoReceiptUrl( @@ -52,12 +53,14 @@ interface ExchangeRules : Feature, Exchanger { } interface ExchangeUrlBuilder { + @Suppress("LongParameterList") fun getUrl( action: CurrencyExchangeManager.Action, blockchain: Blockchain, cryptoCurrencyName: String, fiatCurrencyName: String, walletAddress: String, + isDarkTheme: Boolean, ): String? fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index 39d09fb0dc..6ded86b438 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -79,6 +79,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E cryptoCurrencyName: CryptoCurrencyName, fiatCurrencyName: String, walletAddress: String, + isDarkTheme: Boolean, ): String { if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() @@ -92,6 +93,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E .appendQueryParameter("signature", signature(walletAddress)) .appendQueryParameter("fix_currency", "true") .appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL) + if (isDarkTheme) builder.appendQueryParameter("theme", "1inch") return builder.build().toString() } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 472fd9e6a5..2d4694c975 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -102,6 +102,7 @@ class MoonPayService( cryptoCurrencyName: CryptoCurrencyName, fatCurrency: String, walletAddress: String, + isDarkTheme: Boolean, ): String { if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() @@ -112,6 +113,7 @@ class MoonPayService( .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com") + if (isDarkTheme) uri.appendQueryParameter("theme", "dark") val originalQuery = uri.build().encodedQuery ?: uri.build().toString() val signature = createSignature(originalQuery) diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cdcbc7fd9a..7b669beb49 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -9,11 +9,13 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.tap.common.entities.FiatCurrency +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.exchangeServices.ExchangeService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import org.rekotlin.Action @@ -45,17 +47,22 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl var tangemSdkManager: TangemSdkManager? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default + var exchangeService: ExchangeService? = null fun getActualCard(): CardDTO? { return scanResponse?.card } override fun navigate(action: NavigationAction) { - mainStore?.dispatch(action) + mainStore?.dispatchOnMain(action) } override fun getBackStack(): List = mainStore?.state?.navigationState?.backStack.orEmpty() + override fun popBackStack(screen: AppScreen?) { + mainStore?.dispatchOnMain(NavigationAction.PopBackTo(screen)) + } + override fun dispatch(action: Action) { mainStore?.dispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index d03a7956e9..6a82e6bbb4 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -10,24 +10,31 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.errors.UserCancelledException import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager +import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlin.coroutines.suspendCoroutine @@ -35,21 +42,15 @@ import com.tangem.tap.features.wallet.models.Currency as WalletModelCurrency class DerivationManagerImpl( private val appStateHolder: AppStateHolder, + private val currenciesRepository: CurrenciesRepository, + private val networksRepository: NetworksRepository, ) : DerivationManager { override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> val blockchain = Blockchain.fromNetworkId(currency.networkId) val card = appStateHolder.getActualCard() if (blockchain != null && card != null) { - val appToken = if (currency is NonNativeToken) { - Token( - symbol = currency.symbol, - contractAddress = currency.contractAddress, - decimals = currency.decimalCount, - ) - } else { - null - } + val appToken = getAppToken(currency) val scanResponse = appStateHolder.scanResponse if (scanResponse != null) { val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) @@ -70,6 +71,65 @@ class DerivationManagerImpl( } } + override suspend fun deriveAndAddTokens(currency: Currency) = suspendCoroutine { continuation -> + val selectedUserWallet = requireNotNull( + userWalletsListManager.selectedUserWalletSync, + ) { "selectedUserWallet shouldn't be null" } + val scanResponse = selectedUserWallet.scanResponse + val blockchain = requireNotNull( + Blockchain.fromNetworkId(currency.networkId), + ) { "unsupported blockchain" } + val derivationStyleProvider = scanResponse.derivationStyleProvider + val derivationPath = requireNotNull( + blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath, + ) { "derivationPath shouldn't be null" } + val hasDerivation = scanResponse.hasDerivation( + blockchain, + derivationPath, + ) + if (hasDerivation) { + scope.launch { + addToken( + userWalletId = selectedUserWallet.walletId, + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + continuation.resumeWith(Result.success(derivationPath)) + } + } else { + val blockchainNetwork = BlockchainNetwork(blockchain, scanResponse.derivationStyleProvider) + val appCurrency = WalletModelCurrency.fromBlockchainNetwork( + blockchainNetwork, + getAppToken(currency), + ) + deriveMissingBlockchains( + scanResponse = scanResponse, + currencyList = listOf(appCurrency), + onSuccess = { updatedScanResponse -> + scope.launch { + userWalletsListManager.update( + userWalletId = selectedUserWallet.walletId, + update = { it.copy(scanResponse = updatedScanResponse) }, + ) + addToken( + userWalletId = selectedUserWallet.walletId, + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + continuation.resumeWith(Result.success(derivationPath)) + } + }, + onFailure = { + continuation.resumeWith(Result.failure(it)) + }, + ) + } + } + override fun getDerivationPathForBlockchain(networkId: String): String? { val scanResponse = appStateHolder.scanResponse val blockchain = Blockchain.fromNetworkId(networkId) @@ -91,6 +151,63 @@ class DerivationManagerImpl( return false } + private suspend fun addToken( + userWalletId: UserWalletId, + blockchain: Blockchain, + currency: Currency, + derivationPath: String, + derivationStyleProvider: DerivationStyleProvider, + ) { + val cryptoCurrency = convertCurrency( + blockchain = blockchain, + currency = currency, + derivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + currenciesRepository.addCurrencies( + userWalletId, + listOf(cryptoCurrency), + ) + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(cryptoCurrency.network), + refresh = true, + ) + } + + private fun convertCurrency( + blockchain: Blockchain, + currency: Currency, + derivationPath: String, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency { + val cryptoCurrencyFactory = CryptoCurrencyFactory() + return when (currency) { + is Currency.NativeToken -> { + cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + is NonNativeToken -> { + val sdkToken = Token( + name = currency.name, + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + id = currency.id, + ) + cryptoCurrencyFactory.createToken( + sdkToken = sdkToken, + blockchain = blockchain, + extraDerivationPath = derivationPath, + derivationStyleProvider = derivationStyleProvider, + ) + } + } as CryptoCurrency + } + private fun deriveMissingBlockchains( scanResponse: ScanResponse, currencyList: List, @@ -119,7 +236,7 @@ class DerivationManagerImpl( } scope.launch { - val selectedUserWallet = appStateHolder.userWalletsListManager?.selectedUserWalletSync + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync val result = appStateHolder.tangemSdkManager?.derivePublicKeys( cardId = null, // always ignore cardId in derive task @@ -141,12 +258,10 @@ class DerivationManagerImpl( derivedKeys = updatedDerivedKeys, ) if (selectedUserWallet != null) { - val userWallet = selectedUserWallet.copy( - scanResponse = updatedScanResponse, + userWalletsListManager.update( + userWalletId = selectedUserWallet.walletId, + update = { it.copy(scanResponse = updatedScanResponse) }, ) - - appStateHolder.userWalletsListManager?.save(userWallet, canOverride = true) - appStateHolder.walletStoresManager?.fetch(userWallet, true) } appStateHolder.mainStore?.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) delay(DELAY_SDK_DIALOG_CLOSE) @@ -204,6 +319,17 @@ class DerivationManagerImpl( return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getAppToken(currency: Currency): Token? { + return if (currency is NonNativeToken) { + Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + ) + } else { + null + } + } /** * Simple error handler * for now specifically handle only UserCancelled diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 57afd33a26..b05033931a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -17,6 +17,8 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult @@ -25,6 +27,7 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner +import com.tangem.tap.userWalletsListManager import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -35,6 +38,8 @@ class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val analytics: AnalyticsEventHandler, private val cardSdkConfigRepository: CardSdkConfigRepository, + private val walletManagersFacade: WalletManagersFacade, + private val walletFeatureToggles: WalletFeatureToggles, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -409,9 +414,20 @@ class TransactionManagerImpl( } } - private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { - val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) - val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val walletManager = if (walletFeatureToggles.isRedesignedScreenEnabled) { + val selectedUserWallet = requireNotNull( + userWalletsListManager.selectedUserWalletSync, + ) { "userWallet or userWalletsListManager is null" } + walletManagersFacade.getOrCreateWalletManager( + selectedUserWallet.walletId, + blockchain, + derivationPath, + ) + } else { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) + appStateHolder.walletState?.getWalletManager(blockchainNetwork) + } return requireNotNull(walletManager) { "no wallet manager found" } } diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 0403ad4383..6a9a2abf90 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -10,7 +10,8 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.userwallets.UserWalletIdBuilder +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NativeToken @@ -29,6 +30,8 @@ import com.tangem.tap.features.wallet.models.Currency as WalletCurrency class UserWalletManagerImpl( private val appStateHolder: AppStateHolder, + private val walletManagersFacade: WalletManagersFacade, + private val walletFeatureToggles: WalletFeatureToggles, ) : UserWalletManager { override suspend fun getUserTokens( @@ -86,12 +89,10 @@ class UserWalletManagerImpl( } override fun getWalletId(): String { - return appStateHolder.getActualCard()?.let { - UserWalletIdBuilder.card(it) - .build() - ?.stringValue - } - ?: "" + val selectedUserWallet = requireNotNull( + userWalletsListManager.selectedUserWalletSync, + ) { "selectedUserWallet shouldn't be null" } + return selectedUserWallet.walletId.stringValue } override suspend fun isTokenAdded(currency: Currency, derivationPath: String?): Boolean { @@ -142,13 +143,13 @@ class UserWalletManagerImpl( } } - override fun getWalletAddress(networkId: String, derivationPath: String?): String { + override suspend fun getWalletAddress(networkId: String, derivationPath: String?): String { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) return walletManager.wallet.address } - override fun getLastTransactionHash(networkId: String, derivationPath: String?): String? { + override suspend fun getLastTransactionHash(networkId: String, derivationPath: String?): String? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) return walletManager.wallet.recentTransactions @@ -187,7 +188,7 @@ class UserWalletManagerImpl( return balances } - override fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? { + override suspend fun getNativeTokenBalance(networkId: String, derivationPath: String?): ProxyAmount? { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val walletManager = getActualWalletManager(blockchain, derivationPath) return walletManager.wallet.amounts.firstNotNullOfOrNull { @@ -220,10 +221,24 @@ class UserWalletManagerImpl( appStateHolder.mainStore?.dispatchOnMain(WalletAction.LoadData.Refresh) } - @kotlin.jvm.Throws(IllegalArgumentException::class) - private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { - val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) - return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) { + @Throws(IllegalArgumentException::class) + private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val walletManager = if (walletFeatureToggles.isRedesignedScreenEnabled) { + val selectedUserWallet = requireNotNull( + userWalletsListManager.selectedUserWalletSync, + ) { "userWallet or userWalletsListManager is null" } + walletManagersFacade.getOrCreateWalletManager( + selectedUserWallet.walletId, + blockchain, + derivationPath, + ) + } else { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) + return requireNotNull(appStateHolder.walletState?.getWalletManager(blockchainNetwork)) { + "No wallet manager found" + } + } + return requireNotNull(walletManager) { "No wallet manager found" } } diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 19474ce5cc..b64680c54c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -2,13 +2,19 @@ package com.tangem.tap.proxy.di import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager +import com.tangem.tap.features.details.DarkThemeFeatureToggle import com.tangem.tap.proxy.* import dagger.Module import dagger.Provides @@ -30,9 +36,15 @@ class ProxyModule { @Provides @Singleton - fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager { + fun provideUserWalletManager( + appStateHolder: AppStateHolder, + walletManagersFacade: WalletManagersFacade, + walletFeatureToggles: WalletFeatureToggles, + ): UserWalletManager { return UserWalletManagerImpl( appStateHolder = appStateHolder, + walletManagersFacade = walletManagersFacade, + walletFeatureToggles = walletFeatureToggles, ) } @@ -42,22 +54,38 @@ class ProxyModule { appStateHolder: AppStateHolder, analytics: AnalyticsEventHandler, cardSdkConfigRepository: CardSdkConfigRepository, + walletManagersFacade: WalletManagersFacade, + walletFeatureToggles: WalletFeatureToggles, ): TransactionManager { return TransactionManagerImpl( appStateHolder = appStateHolder, analytics = analytics, cardSdkConfigRepository = cardSdkConfigRepository, + walletManagersFacade = walletManagersFacade, + walletFeatureToggles = walletFeatureToggles, ) } @Provides @Singleton - fun provideDerivationManager(appStateHolder: AppStateHolder): DerivationManager { + fun provideDerivationManager( + appStateHolder: AppStateHolder, + currenciesRepository: CurrenciesRepository, + networksRepository: NetworksRepository, + ): DerivationManager { return DerivationManagerImpl( appStateHolder = appStateHolder, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, ) } + @Provides + @Singleton + fun provideDarkThemeFeatureToggle(featureTogglesManager: FeatureTogglesManager): DarkThemeFeatureToggle { + return DarkThemeFeatureToggle(featureTogglesManager) + } + // regions FeatureConsumers @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c677f70300..08f259fa70 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -3,12 +3,16 @@ package com.tangem.tap.proxy.redux import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.tester.api.TesterRouter -import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.navigation.WalletRouter @@ -16,6 +20,8 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles +import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( @@ -29,12 +35,20 @@ data class DaggerGraphState( val walletConnectRepository: WalletConnectRepository? = null, val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, - val tokenDetailsFeatureToggles: TokenDetailsFeatureToggles? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, + val appStateHolder: AppStateHolder? = null, + val appThemeModeRepository: AppThemeModeRepository? = null, + val balanceHidingRepository: BalanceHidingRepository? = null, + val detailsFeatureToggles: DetailsFeatureToggles? = null, + val walletsRepository: WalletsRepository? = null, + val networksRepository: NetworksRepository? = null, + + // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList + val currenciesRepository: CurrenciesRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/res/color/selector_chip_background.xml b/app/src/main/res/color/selector_chip_background.xml index e0b0431767..16be87b913 100644 --- a/app/src/main/res/color/selector_chip_background.xml +++ b/app/src/main/res/color/selector_chip_background.xml @@ -1,10 +1,10 @@ - - - + + + - - + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_stroke.xml b/app/src/main/res/color/selector_chip_stroke.xml index e4a5d64acc..c2827e783e 100644 --- a/app/src/main/res/color/selector_chip_stroke.xml +++ b/app/src/main/res/color/selector_chip_stroke.xml @@ -1,9 +1,9 @@ - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_text.xml b/app/src/main/res/color/selector_chip_text.xml new file mode 100644 index 0000000000..e205f4e82d --- /dev/null +++ b/app/src/main/res/color/selector_chip_text.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text.xml b/app/src/main/res/color/selector_edit_text.xml index b6c0697011..192514f802 100644 --- a/app/src/main/res/color/selector_edit_text.xml +++ b/app/src/main/res/color/selector_edit_text.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text_secondary.xml b/app/src/main/res/color/selector_edit_text_secondary.xml new file mode 100644 index 0000000000..df06469793 --- /dev/null +++ b/app/src/main/res/color/selector_edit_text_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/img_meet_tangem.webp b/app/src/main/res/drawable-hdpi/img_meet_tangem.webp index e9b4ef6b19..a7c8c0b3f6 100644 Binary files a/app/src/main/res/drawable-hdpi/img_meet_tangem.webp and b/app/src/main/res/drawable-hdpi/img_meet_tangem.webp differ diff --git a/app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp b/app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp new file mode 100644 index 0000000000..64c528cb66 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/img_revolutionary_wallet.webp differ diff --git a/app/src/main/res/drawable-hdpi/img_meet_tangem2.webp b/app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-hdpi/img_meet_tangem2.webp rename to app/src/main/res/drawable-hdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-hdpi/revolutionary_wallet.webp b/app/src/main/res/drawable-hdpi/revolutionary_wallet.webp deleted file mode 100644 index 7e0f631f9c..0000000000 Binary files a/app/src/main/res/drawable-hdpi/revolutionary_wallet.webp and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/img_meet_tangem.webp b/app/src/main/res/drawable-mdpi/img_meet_tangem.webp index 7bd5ec80c5..26a4bd6a9d 100644 Binary files a/app/src/main/res/drawable-mdpi/img_meet_tangem.webp and b/app/src/main/res/drawable-mdpi/img_meet_tangem.webp differ diff --git a/app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp b/app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp new file mode 100644 index 0000000000..67573e0e1c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/img_revolutionary_wallet.webp differ diff --git a/app/src/main/res/drawable-mdpi/img_meet_tangem2.webp b/app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-mdpi/img_meet_tangem2.webp rename to app/src/main/res/drawable-mdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-mdpi/revolutionary_wallet.webp b/app/src/main/res/drawable-mdpi/revolutionary_wallet.webp deleted file mode 100644 index dfc7b106a9..0000000000 Binary files a/app/src/main/res/drawable-mdpi/revolutionary_wallet.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp b/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp index 9bc7cba662..179125d14b 100644 Binary files a/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp and b/app/src/main/res/drawable-xhdpi/img_meet_tangem.webp differ diff --git a/app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp b/app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp new file mode 100644 index 0000000000..020651af9b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/img_revolutionary_wallet.webp differ diff --git a/app/src/main/res/drawable-xhdpi/img_meet_tangem2.webp b/app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xhdpi/img_meet_tangem2.webp rename to app/src/main/res/drawable-xhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xhdpi/revolutionary_wallet.webp b/app/src/main/res/drawable-xhdpi/revolutionary_wallet.webp deleted file mode 100644 index ee0435caf1..0000000000 Binary files a/app/src/main/res/drawable-xhdpi/revolutionary_wallet.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp b/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp index 941a671afb..f34b981aab 100644 Binary files a/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp and b/app/src/main/res/drawable-xxhdpi/img_meet_tangem.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp b/app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp new file mode 100644 index 0000000000..f49cff2ff7 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/img_revolutionary_wallet.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/img_meet_tangem2.webp b/app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxhdpi/img_meet_tangem2.webp rename to app/src/main/res/drawable-xxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.webp b/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.webp deleted file mode 100644 index 360f004149..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/revolutionary_wallet.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp b/app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp new file mode 100644 index 0000000000..bf14c572fb Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/img_meet_tangem.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp b/app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp new file mode 100644 index 0000000000..c7030ebcd2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/img_revolutionary_wallet.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/img_meet_tangem2.webp b/app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp similarity index 100% rename from app/src/main/res/drawable-xxxhdpi/img_meet_tangem2.webp rename to app/src/main/res/drawable-xxxhdpi/img_tangem_for_everyone.webp diff --git a/app/src/main/res/drawable-xxxhdpi/meet_tangem.webp b/app/src/main/res/drawable-xxxhdpi/meet_tangem.webp deleted file mode 100644 index 8ca89394bc..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/meet_tangem.webp and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.webp b/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.webp deleted file mode 100644 index 9568833439..0000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/revolutionary_wallet.webp and /dev/null differ diff --git a/app/src/main/res/drawable/bg_half_transparent_overlay.xml b/app/src/main/res/drawable/bg_half_transparent_overlay.xml index 4b5937d793..1e819d64aa 100644 --- a/app/src/main/res/drawable/bg_half_transparent_overlay.xml +++ b/app/src/main/res/drawable/bg_half_transparent_overlay.xml @@ -2,8 +2,8 @@ \ No newline at end of file diff --git a/app/src/main/res/drawable/card_placeholder_black.xml b/app/src/main/res/drawable/card_placeholder_black.xml deleted file mode 100644 index 83fab8e0f7..0000000000 --- a/app/src/main/res/drawable/card_placeholder_black.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/currency0.webp b/app/src/main/res/drawable/currency0.webp index a55afc36fe..7bc5ca5605 100644 Binary files a/app/src/main/res/drawable/currency0.webp and b/app/src/main/res/drawable/currency0.webp differ diff --git a/app/src/main/res/drawable/currency1.webp b/app/src/main/res/drawable/currency1.webp index b24e1009ff..4818bdf156 100644 Binary files a/app/src/main/res/drawable/currency1.webp and b/app/src/main/res/drawable/currency1.webp differ diff --git a/app/src/main/res/drawable/currency2.webp b/app/src/main/res/drawable/currency2.webp index 86dfb0f2d4..9f726cd395 100644 Binary files a/app/src/main/res/drawable/currency2.webp and b/app/src/main/res/drawable/currency2.webp differ diff --git a/app/src/main/res/drawable/currency3.webp b/app/src/main/res/drawable/currency3.webp index a6a5da8499..4a5a827545 100644 Binary files a/app/src/main/res/drawable/currency3.webp and b/app/src/main/res/drawable/currency3.webp differ diff --git a/app/src/main/res/drawable/currency4.webp b/app/src/main/res/drawable/currency4.webp index 517c55c0f7..2f5bd64aae 100644 Binary files a/app/src/main/res/drawable/currency4.webp and b/app/src/main/res/drawable/currency4.webp differ diff --git a/app/src/main/res/drawable/dapps0.webp b/app/src/main/res/drawable/dapps0.webp index 65809f5fe4..5a5ddd3913 100644 Binary files a/app/src/main/res/drawable/dapps0.webp and b/app/src/main/res/drawable/dapps0.webp differ diff --git a/app/src/main/res/drawable/dapps1.webp b/app/src/main/res/drawable/dapps1.webp index 833c952010..d4895fdfbf 100644 Binary files a/app/src/main/res/drawable/dapps1.webp and b/app/src/main/res/drawable/dapps1.webp differ diff --git a/app/src/main/res/drawable/dapps2.webp b/app/src/main/res/drawable/dapps2.webp index a13cb38ba8..7411af18e6 100644 Binary files a/app/src/main/res/drawable/dapps2.webp and b/app/src/main/res/drawable/dapps2.webp differ diff --git a/app/src/main/res/drawable/dapps3.webp b/app/src/main/res/drawable/dapps3.webp index 99f594fd3d..4fe8d18fa1 100644 Binary files a/app/src/main/res/drawable/dapps3.webp and b/app/src/main/res/drawable/dapps3.webp differ diff --git a/app/src/main/res/drawable/dapps4.webp b/app/src/main/res/drawable/dapps4.webp index 15704d6a6e..57507b303c 100644 Binary files a/app/src/main/res/drawable/dapps4.webp and b/app/src/main/res/drawable/dapps4.webp differ diff --git a/app/src/main/res/drawable/dapps5.webp b/app/src/main/res/drawable/dapps5.webp index 5d6370f05a..94405e826b 100644 Binary files a/app/src/main/res/drawable/dapps5.webp and b/app/src/main/res/drawable/dapps5.webp differ diff --git a/app/src/main/res/drawable/ic_activation_success.xml b/app/src/main/res/drawable/ic_activation_success.xml deleted file mode 100644 index 7957c82493..0000000000 --- a/app/src/main/res/drawable/ic_activation_success.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_arbitrum_no_color.xml b/app/src/main/res/drawable/ic_arbitrum_no_color.xml index 02164f7121..e7c20e844b 100644 --- a/app/src/main/res/drawable/ic_arbitrum_no_color.xml +++ b/app/src/main/res/drawable/ic_arbitrum_no_color.xml @@ -3,7 +3,6 @@ android:width="22dp" xmlns:android="http://schemas.android.com/apk/res/android"> - diff --git a/app/src/main/res/drawable/ic_arrows_up_down.xml b/app/src/main/res/drawable/ic_arrows_up_down.xml index 6e7d34d5e6..96faa98e71 100644 --- a/app/src/main/res/drawable/ic_arrows_up_down.xml +++ b/app/src/main/res/drawable/ic_arrows_up_down.xml @@ -4,6 +4,6 @@ android:viewportWidth="14" android:viewportHeight="19"> diff --git a/app/src/main/res/drawable/ic_avalanche_no_color.xml b/app/src/main/res/drawable/ic_avalanche_no_color.xml index 72317338ab..032533db9d 100644 --- a/app/src/main/res/drawable/ic_avalanche_no_color.xml +++ b/app/src/main/res/drawable/ic_avalanche_no_color.xml @@ -1,6 +1,11 @@ - - - + + diff --git a/app/src/main/res/drawable/ic_azero_no_color.xml b/app/src/main/res/drawable/ic_azero_no_color.xml index da712895b3..612a316f3f 100644 --- a/app/src/main/res/drawable/ic_azero_no_color.xml +++ b/app/src/main/res/drawable/ic_azero_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml b/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml index 8ca8b29ddb..f9a1276588 100644 --- a/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml +++ b/app/src/main/res/drawable/ic_bitcoin_cash_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_bitcoin_no_color.xml b/app/src/main/res/drawable/ic_bitcoin_no_color.xml index 9b8aef2452..b18c3c9cce 100644 --- a/app/src/main/res/drawable/ic_bitcoin_no_color.xml +++ b/app/src/main/res/drawable/ic_bitcoin_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_bsc_no_color.xml b/app/src/main/res/drawable/ic_bsc_no_color.xml index 1fb72febdf..a7062f73c1 100644 --- a/app/src/main/res/drawable/ic_bsc_no_color.xml +++ b/app/src/main/res/drawable/ic_bsc_no_color.xml @@ -1,7 +1,6 @@ - diff --git a/app/src/main/res/drawable/ic_cardano_no_color.xml b/app/src/main/res/drawable/ic_cardano_no_color.xml index ea706040d3..46a9df7709 100644 --- a/app/src/main/res/drawable/ic_cardano_no_color.xml +++ b/app/src/main/res/drawable/ic_cardano_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_chia_no_color.xml b/app/src/main/res/drawable/ic_chia_no_color.xml index 9119f89479..4ed5d9574b 100644 --- a/app/src/main/res/drawable/ic_chia_no_color.xml +++ b/app/src/main/res/drawable/ic_chia_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - - diff --git a/app/src/main/res/drawable/ic_dash_no_color.xml b/app/src/main/res/drawable/ic_dash_no_color.xml index be475ecf51..0e7c3f29b6 100644 --- a/app/src/main/res/drawable/ic_dash_no_color.xml +++ b/app/src/main/res/drawable/ic_dash_no_color.xml @@ -6,9 +6,6 @@ - diff --git a/app/src/main/res/drawable/ic_discord.xml b/app/src/main/res/drawable/ic_discord.xml index 88444ddf14..6502165e77 100644 --- a/app/src/main/res/drawable/ic_discord.xml +++ b/app/src/main/res/drawable/ic_discord.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M19.636,4.924C18.212,4.259 16.689,3.775 15.097,3.5C14.902,3.853 14.673,4.327 14.516,4.705C12.824,4.451 11.147,4.451 9.486,4.705C9.328,4.327 9.095,3.853 8.897,3.5C7.304,3.775 5.779,4.26 4.355,4.927C1.483,9.26 0.704,13.486 1.093,17.651C2.999,19.071 4.845,19.934 6.66,20.498C7.108,19.882 7.508,19.228 7.852,18.538C7.196,18.289 6.568,17.983 5.975,17.626C6.132,17.51 6.286,17.388 6.435,17.263C10.055,18.953 13.988,18.953 17.565,17.263C17.715,17.388 17.869,17.51 18.025,17.626C17.43,17.984 16.8,18.291 16.144,18.54C16.489,19.228 16.886,19.884 17.336,20.5C19.153,19.935 21.001,19.073 22.906,17.651C23.363,12.822 22.126,8.636 19.636,4.924ZM8.345,15.089C7.259,15.089 6.368,14.076 6.368,12.843C6.368,11.61 7.24,10.596 8.345,10.596C9.451,10.596 10.342,11.608 10.323,12.843C10.325,14.076 9.451,15.089 8.345,15.089ZM15.655,15.089C14.568,15.089 13.677,14.076 13.677,12.843C13.677,11.61 14.549,10.596 15.655,10.596C16.76,10.596 17.651,11.608 17.632,12.843C17.632,14.076 16.76,15.089 15.655,15.089Z" + android:fillColor="#909090"/> diff --git a/app/src/main/res/drawable/ic_dogecoin_no_color.xml b/app/src/main/res/drawable/ic_dogecoin_no_color.xml index 8ca48b696e..9d52af3530 100644 --- a/app/src/main/res/drawable/ic_dogecoin_no_color.xml +++ b/app/src/main/res/drawable/ic_dogecoin_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml index 321894f58a..bef54d1532 100644 --- a/app/src/main/res/drawable/ic_dot.xml +++ b/app/src/main/res/drawable/ic_dot.xml @@ -7,7 +7,7 @@ android:thickness="4.5dp" android:useLevel="false"> diff --git a/app/src/main/res/drawable/ic_eth_no_color.xml b/app/src/main/res/drawable/ic_eth_no_color.xml index 95066b1560..41071ba559 100644 --- a/app/src/main/res/drawable/ic_eth_no_color.xml +++ b/app/src/main/res/drawable/ic_eth_no_color.xml @@ -1,7 +1,6 @@ - diff --git a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml index dd68efa83e..1379276e0d 100644 --- a/app/src/main/res/drawable/ic_ethereumfair_no_color.xml +++ b/app/src/main/res/drawable/ic_ethereumfair_no_color.xml @@ -6,9 +6,6 @@ android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml index aa369fd0ab..40a0651a32 100644 --- a/app/src/main/res/drawable/ic_ethereumpow_no_color.xml +++ b/app/src/main/res/drawable/ic_ethereumpow_no_color.xml @@ -6,9 +6,6 @@ android:viewportHeight="22"> - - + + diff --git a/app/src/main/res/drawable/ic_fantom_no_color.xml b/app/src/main/res/drawable/ic_fantom_no_color.xml index 4b873b7e55..f7498b5230 100644 --- a/app/src/main/res/drawable/ic_fantom_no_color.xml +++ b/app/src/main/res/drawable/ic_fantom_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml index 57bbd79618..daa1de7021 100644 --- a/app/src/main/res/drawable/ic_github.xml +++ b/app/src/main/res/drawable/ic_github.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_gnosis_no_color.xml b/app/src/main/res/drawable/ic_gnosis_no_color.xml index a8d4069093..271d24efd6 100644 --- a/app/src/main/res/drawable/ic_gnosis_no_color.xml +++ b/app/src/main/res/drawable/ic_gnosis_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_instagram.xml b/app/src/main/res/drawable/ic_instagram.xml index 844639d9e3..76744cf7f5 100644 --- a/app/src/main/res/drawable/ic_instagram.xml +++ b/app/src/main/res/drawable/ic_instagram.xml @@ -1,10 +1,19 @@ - - - - - - - + + + + + + + diff --git a/app/src/main/res/drawable/ic_kaspa_no_color.xml b/app/src/main/res/drawable/ic_kaspa_no_color.xml index cae1a10de8..55b68fd581 100644 --- a/app/src/main/res/drawable/ic_kaspa_no_color.xml +++ b/app/src/main/res/drawable/ic_kaspa_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_kava_no_color.xml b/app/src/main/res/drawable/ic_kava_no_color.xml index d48b77cede..49e4647c33 100644 --- a/app/src/main/res/drawable/ic_kava_no_color.xml +++ b/app/src/main/res/drawable/ic_kava_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_linkedin.xml b/app/src/main/res/drawable/ic_linkedin.xml index 36600f605d..2df3cb834a 100644 --- a/app/src/main/res/drawable/ic_linkedin.xml +++ b/app/src/main/res/drawable/ic_linkedin.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_litecoin_no_color.xml b/app/src/main/res/drawable/ic_litecoin_no_color.xml index 5758f1ab6e..5b3acc91e8 100644 --- a/app/src/main/res/drawable/ic_litecoin_no_color.xml +++ b/app/src/main/res/drawable/ic_litecoin_no_color.xml @@ -1,9 +1,10 @@ - - - - - - + + diff --git a/app/src/main/res/drawable/ic_near_no_color.xml b/app/src/main/res/drawable/ic_near_no_color.xml new file mode 100644 index 0000000000..573a4ef9be --- /dev/null +++ b/app/src/main/res/drawable/ic_near_no_color.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_octaspace_no_color.xml b/app/src/main/res/drawable/ic_octaspace_no_color.xml index 36751027a3..c61e79358f 100644 --- a/app/src/main/res/drawable/ic_octaspace_no_color.xml +++ b/app/src/main/res/drawable/ic_octaspace_no_color.xml @@ -4,9 +4,6 @@ android:autoMirrored="true" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_optimism_no_color.xml b/app/src/main/res/drawable/ic_optimism_no_color.xml index 3a9d21620b..7091ff4c7d 100644 --- a/app/src/main/res/drawable/ic_optimism_no_color.xml +++ b/app/src/main/res/drawable/ic_optimism_no_color.xml @@ -5,9 +5,6 @@ android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_paste_disabled.xml b/app/src/main/res/drawable/ic_paste_disabled.xml index 7666c5a52d..156ddafc4b 100644 --- a/app/src/main/res/drawable/ic_paste_disabled.xml +++ b/app/src/main/res/drawable/ic_paste_disabled.xml @@ -6,6 +6,6 @@ android:viewportHeight="19"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_polkadot_no_color.xml b/app/src/main/res/drawable/ic_polkadot_no_color.xml index 705c2e1a4b..6e8ee64a05 100644 --- a/app/src/main/res/drawable/ic_polkadot_no_color.xml +++ b/app/src/main/res/drawable/ic_polkadot_no_color.xml @@ -1,7 +1,6 @@ - diff --git a/app/src/main/res/drawable/ic_polygon_no_color.xml b/app/src/main/res/drawable/ic_polygon_no_color.xml index 665486d682..eafe18126e 100644 --- a/app/src/main/res/drawable/ic_polygon_no_color.xml +++ b/app/src/main/res/drawable/ic_polygon_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_qr_code_scan.xml b/app/src/main/res/drawable/ic_qr_code_scan.xml index 34a2bf62a8..5ec455e546 100644 --- a/app/src/main/res/drawable/ic_qr_code_scan.xml +++ b/app/src/main/res/drawable/ic_qr_code_scan.xml @@ -5,5 +5,5 @@ android:viewportHeight="18"> + android:fillColor="@color/text_primary_1" /> diff --git a/app/src/main/res/drawable/ic_ravencoin_no_color.xml b/app/src/main/res/drawable/ic_ravencoin_no_color.xml index 5bc49c57cf..7599b5c971 100644 --- a/app/src/main/res/drawable/ic_ravencoin_no_color.xml +++ b/app/src/main/res/drawable/ic_ravencoin_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - + + diff --git a/app/src/main/res/drawable/ic_rsk_no_color.xml b/app/src/main/res/drawable/ic_rsk_no_color.xml index e2bdadb7ac..f09adebd5c 100644 --- a/app/src/main/res/drawable/ic_rsk_no_color.xml +++ b/app/src/main/res/drawable/ic_rsk_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_selected_dot.xml b/app/src/main/res/drawable/ic_selected_dot.xml index e847725fc2..f747ab54ba 100644 --- a/app/src/main/res/drawable/ic_selected_dot.xml +++ b/app/src/main/res/drawable/ic_selected_dot.xml @@ -6,7 +6,7 @@ android:shape="ring" android:thickness="4.5dp" android:useLevel="false"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_solana_no_color.xml b/app/src/main/res/drawable/ic_solana_no_color.xml index a49a56f643..31e6195232 100644 --- a/app/src/main/res/drawable/ic_solana_no_color.xml +++ b/app/src/main/res/drawable/ic_solana_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_stellar_no_color.xml b/app/src/main/res/drawable/ic_stellar_no_color.xml index 27a497f3fb..61e3683e6f 100644 --- a/app/src/main/res/drawable/ic_stellar_no_color.xml +++ b/app/src/main/res/drawable/ic_stellar_no_color.xml @@ -3,7 +3,6 @@ android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android"> - diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml index 5d2351d73e..020ee3bc71 100644 --- a/app/src/main/res/drawable/ic_telegram.xml +++ b/app/src/main/res/drawable/ic_telegram.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_telos_no_color.xml b/app/src/main/res/drawable/ic_telos_no_color.xml index 01aca1d2b2..bdd073cd40 100644 --- a/app/src/main/res/drawable/ic_telos_no_color.xml +++ b/app/src/main/res/drawable/ic_telos_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_terra_no_color.xml b/app/src/main/res/drawable/ic_terra_no_color.xml index 0ba5bf40f7..f25adb981e 100644 --- a/app/src/main/res/drawable/ic_terra_no_color.xml +++ b/app/src/main/res/drawable/ic_terra_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - diff --git a/app/src/main/res/drawable/ic_tezos_no_color.xml b/app/src/main/res/drawable/ic_tezos_no_color.xml index c023c94fa3..a929bead46 100644 --- a/app/src/main/res/drawable/ic_tezos_no_color.xml +++ b/app/src/main/res/drawable/ic_tezos_no_color.xml @@ -1,6 +1,5 @@ - diff --git a/app/src/main/res/drawable/ic_ton_no_color.xml b/app/src/main/res/drawable/ic_ton_no_color.xml index c3d5d856db..e9ecd65434 100644 --- a/app/src/main/res/drawable/ic_ton_no_color.xml +++ b/app/src/main/res/drawable/ic_ton_no_color.xml @@ -3,9 +3,6 @@ android:height="22dp" android:viewportWidth="22" android:viewportHeight="22"> - - diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml index de09724525..1c3cb6cfef 100644 --- a/app/src/main/res/drawable/ic_twitter.xml +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_walletconnect.xml b/app/src/main/res/drawable/ic_walletconnect.xml index 925a858a8a..d4b4c99942 100644 --- a/app/src/main/res/drawable/ic_walletconnect.xml +++ b/app/src/main/res/drawable/ic_walletconnect.xml @@ -1,5 +1,7 @@ - + diff --git a/app/src/main/res/drawable/ic_xrp_no_color.xml b/app/src/main/res/drawable/ic_xrp_no_color.xml index 67185b115a..47a43000ca 100644 --- a/app/src/main/res/drawable/ic_xrp_no_color.xml +++ b/app/src/main/res/drawable/ic_xrp_no_color.xml @@ -1,6 +1,10 @@ - - - + + diff --git a/app/src/main/res/drawable/ic_youtube.xml b/app/src/main/res/drawable/ic_youtube.xml index 81eb8d7e8c..6d5ade9d08 100644 --- a/app/src/main/res/drawable/ic_youtube.xml +++ b/app/src/main/res/drawable/ic_youtube.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ill_reset_background.xml b/app/src/main/res/drawable/ill_reset_background.xml deleted file mode 100644 index 8ce3137945..0000000000 --- a/app/src/main/res/drawable/ill_reset_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/img_alert.xml b/app/src/main/res/drawable/img_alert.xml new file mode 100644 index 0000000000..42b087d91c --- /dev/null +++ b/app/src/main/res/drawable/img_alert.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/img_onboarding_success.xml b/app/src/main/res/drawable/img_onboarding_success.xml new file mode 100644 index 0000000000..84574803ea --- /dev/null +++ b/app/src/main/res/drawable/img_onboarding_success.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/shape_ellipse.xml b/app/src/main/res/drawable/shape_ellipse.xml index b5f3fc7939..ca6cad24b5 100644 --- a/app/src/main/res/drawable/shape_ellipse.xml +++ b/app/src/main/res/drawable/shape_ellipse.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/drawable/shape_refresh_button.xml b/app/src/main/res/drawable/shape_refresh_button.xml index ac9508438d..cade7f63f3 100644 --- a/app/src/main/res/drawable/shape_refresh_button.xml +++ b/app/src/main/res/drawable/shape_refresh_button.xml @@ -2,11 +2,11 @@ - + + android:color="@color/button_secondary" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index 32d2544981..131299d4d5 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -85,7 +85,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" + android:indeterminateTint="@color/background_secondary" android:visibility="invisible" /> @@ -93,6 +93,9 @@ + app:srcCompat="@drawable/ic_angle_bracket_up" + app:tint="@color/icon_primary_1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_onboarding_address_info.xml b/app/src/main/res/layout/dialog_onboarding_address_info.xml index eadb8f7f36..b4a422e77b 100644 --- a/app/src/main/res/layout/dialog_onboarding_address_info.xml +++ b/app/src/main/res/layout/dialog_onboarding_address_info.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="502dp"> + android:layout_height="wrap_content" + android:background="@color/background_primary"> @@ -52,13 +54,14 @@ android:layout_marginStart="32dp" android:layout_marginEnd="8dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" app:layout_constraintEnd_toStartOf="@+id/btn_fl_share" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + android:backgroundTint="@color/button_secondary" + app:layout_constraintTop_toBottomOf="@id/tv_receive_message"> @@ -98,13 +103,14 @@ android:layout_height="40dp" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" + android:backgroundTint="@color/background_primary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/btn_fl_copy_address" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + app:layout_constraintTop_toBottomOf="@+id/tv_receive_message"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml index 29bf1c052b..a666076b95 100644 --- a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml +++ b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:minHeight="420dp" tools:layout_gravity="bottom"> @@ -25,7 +25,7 @@ android:layout_width="32dp" android:layout_height="32dp" android:background="@drawable/shape_circle" - android:backgroundTint="@color/backgroundWhite" + android:backgroundTint="@color/background_primary" app:layout_constraintBottom_toBottomOf="@id/iv_cross" app:layout_constraintEnd_toEndOf="@id/iv_cross" app:layout_constraintStart_toStartOf="@id/iv_cross" @@ -55,7 +55,7 @@ android:layout_marginEnd="38dp" android:text="@string/russian_bank_card_warning_title" android:textAlignment="center" - android:textColor="@color/textBlack" + android:textColor="@color/text_primary_1" android:textSize="20sp" app:layout_constraintBottom_toTopOf="@id/tv_description" app:layout_constraintEnd_toEndOf="parent" @@ -72,7 +72,7 @@ android:layout_marginBottom="38dp" android:text="@string/russian_bank_card_warning_subtitle" android:textAlignment="center" - android:textColor="@color/textBlack" + android:textColor="@color/text_primary_1" android:textSize="14sp" app:layout_constraintBottom_toTopOf="@id/btn_yes" app:layout_constraintEnd_toEndOf="parent" @@ -87,9 +87,9 @@ android:layout_marginStart="16dp" android:layout_marginEnd="6dp" android:layout_marginBottom="38dp" - android:backgroundTint="@color/tapButtonColorBlack" + android:backgroundTint="@color/button_primary" android:text="@string/common_yes" - android:textColor="@color/white" + android:textColor="@color/text_primary_2" app:cornerRadius="14dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/btn_no" @@ -102,9 +102,9 @@ android:layout_height="wrap_content" android:layout_marginStart="6dp" android:layout_marginEnd="16dp" - android:backgroundTint="@color/buttonGray" + android:backgroundTint="@color/button_secondary" android:text="@string/common_no" - android:textColor="@color/textBlack" + android:textColor="@color/text_primary_1" app:cornerRadius="14dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/btn_yes" diff --git a/app/src/main/res/layout/dialog_wallet_trade.xml b/app/src/main/res/layout/dialog_wallet_trade.xml index dea867e05a..96dd6d34c1 100644 --- a/app/src/main/res/layout/dialog_wallet_trade.xml +++ b/app/src/main/res/layout/dialog_wallet_trade.xml @@ -11,7 +11,7 @@ android:layout_height="56dp" android:padding="16dp" android:text="@string/wallet_choose_trade_action" - android:textColor="@color/darkGray2" + android:textColor="@color/text_secondary" android:textSize="14sp" /> @@ -39,7 +39,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/common_sell" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_arrow_down_24" /> @@ -54,7 +54,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/swapping_swap_action" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_exchange_vertical_24" /> diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 328a435e5c..1c237b7938 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -21,6 +21,8 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" + app:navigationIconTint="@color/icon_primary_1" + app:titleTextColor="@color/text_primary_1" app:title="@string/disclaimer_title" /> @@ -29,7 +31,7 @@ android:id="@+id/cl_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:background="@color/background_secondary" app:layout_behavior="@string/appbar_scrolling_view_behavior"> diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 821f0d3ef8..217d2db70e 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_primary" android:clipChildren="false" android:clipToPadding="false" android:orientation="vertical"> @@ -21,8 +21,10 @@ @@ -59,7 +61,7 @@ android:layout_height="236dp" android:adjustViewBounds="true" android:background="@drawable/shape_circle" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/background_primary" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@id/fl_cards_container" @@ -113,17 +115,35 @@ - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 20949f4932..a7f8c54c42 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_wallet" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -22,7 +22,9 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" - app:title="@string/common_send" /> + app:navigationIconTint="@color/text_primary_1" + app:title="@string/common_send" + app:titleTextColor="@color/text_primary_1" /> @@ -125,7 +127,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" /> + android:indeterminateTint="@color/background_primary" /> diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 3d6e2f4020..48384d34c6 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:clipChildren="false" android:clipToPadding="false" android:focusableInTouchMode="true" @@ -16,7 +16,7 @@ style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:fitsSystemWindows="true" app:liftOnScroll="true"> diff --git a/app/src/main/res/layout/item_backup_info_adapter.xml b/app/src/main/res/layout/item_backup_info_adapter.xml index ecbfdbdb1b..469434318b 100644 --- a/app/src/main/res/layout/item_backup_info_adapter.xml +++ b/app/src/main/res/layout/item_backup_info_adapter.xml @@ -2,7 +2,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + android:layout_height="match_parent" + android:background="@color/background_primary"> + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/icon_informative" /> @@ -65,6 +67,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_2" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_2_title" /> @@ -98,6 +101,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_3" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_3_title" /> @@ -117,10 +121,10 @@ @@ -28,12 +29,17 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="40dp" android:hint="@string/onboarding_wallet_info_title_third" + android:textColorHint="@color/text_primary_1" android:theme="@style/EditTextThemeOverlay" app:boxStrokeColor="@color/selector_edit_text" app:boxStrokeWidth="1dp" app:endIconDrawable="@drawable/selector_password_toggle" + app:boxStrokeErrorColor="@color/icon_warning" app:endIconMode="password_toggle" app:hintTextColor="@color/accent" + app:endIconTint="@color/icon_primary_1" + app:errorIconTint="@color/icon_warning" + app:errorTextColor="@color/icon_warning" app:layout_constraintTop_toBottomOf="@id/tv_access_code_enter_description"> @@ -48,10 +55,10 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -75,7 +78,9 @@ @@ -93,6 +98,9 @@ - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml index c1bc683d3a..361f8cbb40 100644 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ b/app/src/main/res/layout/layout_pseudo_toolbar.xml @@ -4,7 +4,7 @@ android:id="@+id/pseudo_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/backgroundLightGray" + android:background="@color/background_primary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -18,6 +18,7 @@ android:clickable="true" android:focusable="true" android:padding="16dp" + android:tint="@color/icon_primary_1" android:src="@drawable/ic_close_24" /> diff --git a/app/src/main/res/layout/layout_receipt_total.xml b/app/src/main/res/layout/layout_receipt_total.xml index 7cca4f9165..5ff5d3377c 100644 --- a/app/src/main/res/layout/layout_receipt_total.xml +++ b/app/src/main/res/layout/layout_receipt_total.xml @@ -47,7 +47,7 @@ android:layout_gravity="end" android:layout_marginTop="4dp" android:gravity="end" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" /> @@ -64,7 +64,7 @@ android:layout_height="wrap_content" android:layout_gravity="start" android:text="@string/send_total_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" /> @@ -74,7 +74,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" tools:text="usd" /> diff --git a/app/src/main/res/layout/layout_send_address.xml b/app/src/main/res/layout/layout_send_address.xml index c6dd2873c8..8367bf3524 100644 --- a/app/src/main/res/layout/layout_send_address.xml +++ b/app/src/main/res/layout/layout_send_address.xml @@ -21,23 +21,24 @@ android:id="@+id/tilAddress" android:layout_width="0dp" android:layout_height="wrap_content" - app:boxBackgroundColor="@color/backgroundLightGray" app:errorIconDrawable="@null" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" + style="@style/SecondaryTextInputLayout" app:layout_constraintTop_toTopOf="parent"> @@ -67,10 +68,10 @@ android:id="@+id/flQrCode" android:layout_width="@dimen/btn_rounded_size" android:layout_height="@dimen/btn_rounded_size" - android:layout_marginTop="10dp" android:background="@drawable/shape_ellipse" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/tilAddress"> + android:layout_marginTop="4dp" + app:layout_constraintTop_toTopOf="parent"> @@ -74,7 +75,7 @@ android:drawablePadding="10dp" android:fontFamily="sans-serif-light" android:textAllCaps="true" - android:textColor="@color/blue" + android:textColor="@color/accent" android:textSize="32sp" app:drawableEndCompat="@drawable/ic_arrows_up_down" app:layout_constraintEnd_toEndOf="parent" @@ -88,7 +89,7 @@ android:layout_gravity="end" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/flAmountToSend" /> diff --git a/app/src/main/res/layout/layout_send_fee.xml b/app/src/main/res/layout/layout_send_fee.xml index f54f8f8bfe..bba30587f9 100644 --- a/app/src/main/res/layout/layout_send_fee.xml +++ b/app/src/main/res/layout/layout_send_fee.xml @@ -85,6 +85,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="8dp" android:text="@string/send_fee_include_description" + android:textColor="@color/text_primary_1" android:textSize="13sp" /> diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml index 2cf18b1ed5..4a8fab6816 100644 --- a/app/src/main/res/layout/layout_send_receipt.xml +++ b/app/src/main/res/layout/layout_send_receipt.xml @@ -34,7 +34,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/send_fee_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvReceiptAmount" /> @@ -59,7 +59,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tvReceiptFee" diff --git a/app/src/main/res/layout/layout_single_wallet_balance.xml b/app/src/main/res/layout/layout_single_wallet_balance.xml index 984d73bf24..c3a75693bc 100644 --- a/app/src/main/res/layout/layout_single_wallet_balance.xml +++ b/app/src/main/res/layout/layout_single_wallet_balance.xml @@ -29,78 +29,73 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:paddingBottom="4dp" + android:src="@drawable/img_warning_triangle_24" + android:scaleType="center" + android:background="@drawable/shape_ellipse" + android:backgroundTint="@color/buttonGray" + android:contentDescription="@null" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_warning_card_action.xml b/app/src/main/res/layout/layout_warning_card_action.xml index e85e77d3e5..bbb58fab01 100644 --- a/app/src/main/res/layout/layout_warning_card_action.xml +++ b/app/src/main/res/layout/layout_warning_card_action.xml @@ -45,17 +45,18 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/warning_content_container" /> -