Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-06 10:45:51 +03:00
parent 56956a0c2b
commit b73d93928b
22 changed files with 292 additions and 264 deletions

View file

@ -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()
}

View file

@ -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<AppThemeMode?>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
@ -130,25 +126,24 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
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<AppThemeMode?> {
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)
}
}

View file

@ -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<GlobalSettingsState> = createMainStateFlow()
private fun createMainStateFlow(): StateFlow<GlobalSettingsState> {
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,
)
}
}

View file

@ -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() {

View file

@ -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

View file

@ -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

View file

@ -95,7 +95,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
Analytics.send(IntroductionProcess.ButtonScanCard())
lifecycleScope.launch {
store.dispatch(
HomeAction.ReadCard(lifecycleCoroutineScope = lifecycleScope),
HomeAction.ReadCard(scope = this),
)
}
},

View file

@ -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()

View file

@ -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 {

View file

@ -6,5 +6,6 @@ import android.content.Intent
[REDACTED_AUTHOR]
*/
interface IntentHandler {
suspend fun handleIntent(intent: Intent?): Boolean
fun handleIntent(intent: Intent?): Boolean
}

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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
}
}

View file

@ -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)

View file

@ -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

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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<WelcomeViewModel>()
override fun onStart() {
super.onStart()
Analytics.eraseContext()
@ -43,6 +41,9 @@ internal class WelcomeFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<WelcomeViewModel>()
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"
}
}

View file

@ -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<WelcomeState>,
DefaultLifecycleObserver {
private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY]
internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
private val stateInternal = MutableStateFlow(WelcomeScreenState())
val state: StateFlow<WelcomeScreenState> = 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<WelcomeState> {
}
override fun onCleared() {
store.dispatch(WelcomeAction.ClearCoroutineScope)
store.unsubscribe(this)
}

View file

@ -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