diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt new file mode 100644 index 0000000000..b90ec7a20f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt new file mode 100644 index 0000000000..07a92373e1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt new file mode 100644 index 0000000000..94c08e4f70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt new file mode 100644 index 0000000000..68f1c6121f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt new file mode 100644 index 0000000000..1a90bd0bc5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt new file mode 100644 index 0000000000..143654b18d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt @@ -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 { + + 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 = 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt index 701042816e..3ede2ae446 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt @@ -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 = {}, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt new file mode 100644 index 0000000000..8b90cbbdc9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt @@ -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 { + override val values: Sequence + 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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt index 23661bc2ad..c9bbbdf31c 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt @@ -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),