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 01bdd66eb8..9f1970f662 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,18 +1,20 @@ package com.tangem.tap -import android.app.Application +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 @@ -32,7 +34,6 @@ 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 @@ -49,19 +50,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 @@ -117,8 +114,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() @@ -130,25 +126,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - val splashScreen = installSplashScreen() - - if (!isDarkThemeFeatureEnabled(application)) { - setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) - } + installSplashScreen() + installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation super.onCreate(savedInstanceState) - cardSdkLifecycleObserver.onCreate(context = this) - - bootstrapMainStateUpdates(application) - - 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) @@ -157,11 +152,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac initUserWalletsListManager() initIntentHandlers() - store.dispatch( - ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this), - ), - ) store.dispatch( DaggerGraphAction.SetActivityDependencies( testerRouter = testerRouter, @@ -174,9 +164,57 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } - private fun isDarkThemeFeatureEnabled(application: Application): Boolean { - val featureToggle = (application as TapApplication).darkThemeFeatureToggle - return featureToggle.isDarkThemeEnabled + 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() { @@ -189,7 +227,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() { @@ -207,12 +245,7 @@ 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()) @@ -230,38 +263,21 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) } - private fun bootstrapMainStateUpdates(application: Application) { - viewModel.state - .onEach { state -> - isInitializing = state is GlobalSettingsState.Loading + private fun updateAppTheme(appThemeMode: AppThemeMode) { + MutableAppThemeModeHolder.value = appThemeMode + MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() - when (state) { - is GlobalSettingsState.Content -> { - if (isDarkThemeFeatureEnabled(application)) { - MutableAppThemeModeHolder.value = state.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 + } - val mode = when (state.appThemeMode) { - AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES - AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO - AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - setDefaultNightMode(mode) - } else { - MutableAppThemeModeHolder.value = AppThemeMode.FORCE_LIGHT - } - } - is GlobalSettingsState.Loading -> Unit - } - } - .launchIn(lifecycleScope) + setDefaultNightMode(mode) } private fun isDarkTheme(): Boolean { - return when ( - resources.configuration.uiMode and - Configuration.UI_MODE_NIGHT_MASK - ) { + 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 @@ -269,17 +285,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - private fun systemActions() { - WindowCompat.setDecorFitsSystemWindows(window, false) - - supportFragmentManager.registerFragmentLifecycleCallbacks( - NavBarInsetsFragmentLifecycleCallback(), - true, - ) - - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) @@ -341,43 +346,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 341ee0112d..6992e82d7e 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager 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 @@ -200,6 +201,9 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var appRatingRepository: AppRatingRepository + + @Inject + lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase // endregion Injected override fun onCreate() { 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/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 54b6313c9c..7b5691b79d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -57,7 +57,7 @@ class TangemSdkManager( get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics val needEnrollBiometrics: Boolean - get() = tangemSdk.authenticationManager.canEnrollBiometrics + get() = tangemSdk.authenticationManager.needEnrollBiometrics val keystoreManager: KeystoreManager get() = tangemSdk.keystoreManager 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 2127b6e8c6..12a6e857cd 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 @@ -95,7 +95,7 @@ class HomeFragment : Fragment(), StoreSubscriber { Analytics.send(IntroductionProcess.ButtonScanCard()) lifecycleScope.launch { store.dispatch( - HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleScope), + HomeAction.ReadCard(scope = this), ) } }, 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..c15ba54403 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 { @@ -23,7 +23,7 @@ sealed class HomeAction : Action { */ 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/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 83c294bfb3..04c138ce0e 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 @@ -14,7 +14,7 @@ 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 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/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index e686d2f730..b46578b48b 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 @@ -152,7 +152,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.lifecycleCoroutineScope)) } else { val backupState = store.state.onboardingWalletState.backupState val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) 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..164ef24c14 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,6 +1,6 @@ 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 @@ -18,9 +18,11 @@ 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 +41,94 @@ 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 -> + 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 { + 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 suspend fun disableUserWalletsSaving() { userWalletsListManager.clear() .flatMap { walletStoresManager.clear() } .flatMap { tangemSdkManager.clearSavedUserCodes() } @@ -60,48 +141,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/gradle/dependencies.toml b/gradle/dependencies.toml index 3ecbc0bcfb..f0b0c1d42f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -84,7 +84,7 @@ okHttp-prettyLogging = "3.1.0" # region Tangem tangemBlockchainSdk = "develop-354" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-300" +tangemCardSdk = "develop-302" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem