Updated on 2026-08-14
This commit is contained in:
commit
1e7ef6ec50
9 changed files with 355 additions and 3 deletions
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.tap.features.welcome.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.tap.features.welcome.model.WelcomeModel
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultWelcomeComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted params: WelcomeComponent.Params,
|
||||
) : WelcomeComponent, AppComponentContext by context {
|
||||
|
||||
private val model: WelcomeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.state.collectAsStateWithLifecycle()
|
||||
|
||||
WelcomeScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : WelcomeComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.tap.features.welcome.component
|
||||
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface WelcomeComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val intent: SerializableIntent?,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, WelcomeComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.features.welcome.component.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
||||
import com.tangem.tap.features.welcome.ui.components.WelcomeScreen
|
||||
|
||||
internal class PreviewWelcomeComponent(
|
||||
private val initialState: WelcomeScreenState = WelcomeScreenState(),
|
||||
) : WelcomeComponent {
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
WelcomeScreen(
|
||||
modifier = modifier,
|
||||
state = initialState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.features.welcome.di
|
||||
|
||||
import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.tap.features.welcome.di
|
||||
|
||||
import com.tangem.core.decompose.di.DecomposeComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.tap.features.welcome.model.WelcomeModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(DecomposeComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(WelcomeModel::class)
|
||||
fun bindWelcomeModel(model: WelcomeModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.tangem.tap.features.welcome.model
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
||||
import com.tangem.tap.common.analytics.events.SignIn
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeState
|
||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
// FIXME: Remove redux: [REDACTED_JIRA]
|
||||
internal class WelcomeModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appFinisher: AppFinisher,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), StoreSubscriber<WelcomeState> {
|
||||
|
||||
private val params: WelcomeComponent.Params = paramsContainer.require()
|
||||
private val initialState: WelcomeScreenState = WelcomeScreenState(
|
||||
onPopBack = appFinisher::finish,
|
||||
onUnlockClick = this::unlockWallets,
|
||||
onScanCardClick = this::scanCard,
|
||||
onCloseError = this::closeError,
|
||||
)
|
||||
|
||||
val state: MutableStateFlow<WelcomeScreenState> = MutableStateFlow(initialState)
|
||||
|
||||
init {
|
||||
store.dispatch(WelcomeAction.SetCoroutineScope(modelScope))
|
||||
|
||||
subscribeToStoreChanges()
|
||||
initGlobalState()
|
||||
|
||||
val welcomeAction = if (params.intent != null) {
|
||||
WelcomeAction.ProceedWithIntent(params.intent.toIntent())
|
||||
} else {
|
||||
WelcomeAction.ProceedWithBiometrics()
|
||||
}
|
||||
|
||||
store.dispatch(welcomeAction)
|
||||
}
|
||||
|
||||
private fun unlockWallets() {
|
||||
Analytics.send(SignIn.ButtonBiometricSignIn())
|
||||
store.dispatch(WelcomeAction.ProceedWithBiometrics())
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
Analytics.send(SignIn.ButtonCardSignIn())
|
||||
store.dispatch(WelcomeAction.ProceedWithCard)
|
||||
}
|
||||
|
||||
private fun closeError() {
|
||||
store.dispatch(WelcomeAction.CloseError)
|
||||
}
|
||||
|
||||
override fun newState(state: WelcomeState) {
|
||||
val warning = createWarningIfNeeded(state.error)
|
||||
|
||||
this.state.update { prevState ->
|
||||
prevState.copy(
|
||||
showUnlockWithBiometricsProgress = state.isUnlockWithBiometricsInProgress,
|
||||
showUnlockWithCardProgress = state.isUnlockWithCardInProgress,
|
||||
warning = warning,
|
||||
error = state.error
|
||||
?.takeIf { !it.silent && warning == null }
|
||||
?.let { e ->
|
||||
e.messageResId?.let { TextReference.Res(it) }
|
||||
?: TextReference.Str(e.customMessage)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(WelcomeAction.ClearCoroutineScope)
|
||||
store.unsubscribe(subscriber = this)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createWarningIfNeeded(error: TangemError?): WarningModel? {
|
||||
return when (error) {
|
||||
is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning(
|
||||
isPermanent = error.isPermanent,
|
||||
onDismiss = this::dismissWarning,
|
||||
)
|
||||
is UserWalletsListError.AllKeysInvalidated,
|
||||
is UserWalletsListError.NoUserWalletSelected,
|
||||
-> WarningModel.KeyInvalidatedWarning(
|
||||
onDismiss = this::dismissWarning,
|
||||
)
|
||||
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
|
||||
onDismiss = this::clearUserWallets,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissWarning() {
|
||||
state.update { prevState ->
|
||||
prevState.copy(
|
||||
warning = null,
|
||||
)
|
||||
}
|
||||
closeError()
|
||||
}
|
||||
|
||||
private fun clearUserWallets() {
|
||||
store.dispatch(WelcomeAction.ClearUserWallets)
|
||||
}
|
||||
|
||||
private fun subscribeToStoreChanges() {
|
||||
store.subscribe(this) { appState ->
|
||||
appState.skip { old, new -> old.welcomeState == new.welcomeState }
|
||||
.select { it.welcomeState }
|
||||
}
|
||||
}
|
||||
|
||||
private fun initGlobalState() {
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
store.dispatch(GlobalAction.ExchangeManager.Init)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
package com.tangem.tap.features.welcome.ui
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
||||
|
||||
@Immutable
|
||||
internal data class WelcomeScreenState(
|
||||
val onPopBack: () -> Unit = {},
|
||||
val showUnlockWithBiometricsProgress: Boolean = false,
|
||||
val showUnlockWithCardProgress: Boolean = false,
|
||||
val warning: WarningModel? = null,
|
||||
val error: TextReference? = null,
|
||||
val onUnlockClick: () -> Unit = {},
|
||||
val onScanCardClick: () -> Unit = {},
|
||||
val onCloseError: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package com.tangem.tap.features.welcome.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.SnackbarHost
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.runtime.*
|
||||
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.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent
|
||||
import com.tangem.tap.features.welcome.ui.WelcomeScreenState
|
||||
import com.tangem.tap.features.welcome.ui.model.WarningModel
|
||||
|
||||
@Composable
|
||||
internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference())
|
||||
val warning by rememberUpdatedState(newValue = state.warning)
|
||||
|
||||
BackHandler(onBack = state.onPopBack)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
WelcomeScreenContent(
|
||||
showUnlockProgress = state.showUnlockWithBiometricsProgress,
|
||||
showScanCardProgress = state.showUnlockWithCardProgress,
|
||||
onUnlockClick = state.onUnlockClick,
|
||||
onScanCardClick = state.onScanCardClick,
|
||||
)
|
||||
|
||||
SnackbarHost(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(vertical = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
hostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
WarningDialog(warning)
|
||||
|
||||
LaunchedEffect(errorMessage, state.onCloseError) {
|
||||
errorMessage?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
state.onCloseError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun Preview_WelcomeScreen(
|
||||
@PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
component.Content(Modifier)
|
||||
}
|
||||
}
|
||||
|
||||
private class WelcomeComponentPreviewProvider : PreviewParameterProvider<WelcomeComponent> {
|
||||
override val values: Sequence<WelcomeComponent>
|
||||
get() = sequenceOf(
|
||||
PreviewWelcomeComponent(),
|
||||
PreviewWelcomeComponent(
|
||||
initialState = WelcomeScreenState(
|
||||
showUnlockWithBiometricsProgress = true,
|
||||
showUnlockWithCardProgress = true,
|
||||
),
|
||||
),
|
||||
PreviewWelcomeComponent(
|
||||
initialState = WelcomeScreenState(
|
||||
error = TextReference.Str(value = "Error"),
|
||||
),
|
||||
),
|
||||
PreviewWelcomeComponent(
|
||||
initialState = WelcomeScreenState(
|
||||
warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -24,8 +24,12 @@ internal fun WelcomeScreenContent(
|
|||
showScanCardProgress: Boolean,
|
||||
onUnlockClick: () -> Unit,
|
||||
onScanCardClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerHMax()
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size96),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue