diff --git a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt index 126fac27..008f3b6c 100644 --- a/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt +++ b/core/data/api/src/main/kotlin/com/ninecraft/booket/core/data/api/repository/AuthRepository.kt @@ -1,5 +1,6 @@ package com.ninecraft.booket.core.data.api.repository +import com.ninecraft.booket.core.model.LoginMethod import com.ninecraft.booket.core.model.state.AutoLoginState import com.ninecraft.booket.core.model.state.UserState import kotlinx.coroutines.flow.Flow @@ -19,4 +20,10 @@ interface AuthRepository { val userState: Flow suspend fun getCurrentUserState(): UserState + + val recentLoginMethod: Flow + + suspend fun setRecentLoginMethod(loginMethod: LoginMethod) + + suspend fun clearRecentLoginMethod() } diff --git a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt index bf7b0097..5fdd101f 100644 --- a/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt +++ b/core/data/impl/src/main/kotlin/com/ninecraft/booket/core/data/impl/repository/DefaultAuthRepository.kt @@ -2,8 +2,10 @@ package com.ninecraft.booket.core.data.impl.repository import com.ninecraft.booket.core.common.utils.runSuspendCatching import com.ninecraft.booket.core.data.api.repository.AuthRepository +import com.ninecraft.booket.core.datastore.api.datasource.LoginMethodDataSource import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.di.DataScope +import com.ninecraft.booket.core.model.LoginMethod import com.ninecraft.booket.core.model.state.AutoLoginState import com.ninecraft.booket.core.model.state.UserState import com.ninecraft.booket.core.network.request.LoginRequest @@ -17,6 +19,7 @@ import kotlinx.coroutines.flow.map class DefaultAuthRepository( private val service: ReedService, private val tokenDataSource: TokenDataSource, + private val loginMethodDataSource: LoginMethodDataSource, ) : AuthRepository { override suspend fun login( providerType: String, @@ -39,6 +42,7 @@ class DefaultAuthRepository( override suspend fun withdraw() = runSuspendCatching { service.withdraw() clearTokens() + clearRecentLoginMethod() } private suspend fun saveTokens(accessToken: String, refreshToken: String) { @@ -66,4 +70,14 @@ class DefaultAuthRepository( val accessToken = tokenDataSource.getAccessToken() return if (accessToken.isBlank()) UserState.Guest else UserState.LoggedIn } + + override val recentLoginMethod = loginMethodDataSource.recentLoginMethod + + override suspend fun setRecentLoginMethod(loginMethod: LoginMethod) { + loginMethodDataSource.setRecentLoginMethod(loginMethod) + } + + override suspend fun clearRecentLoginMethod() { + loginMethodDataSource.clearRecentLoginMethod() + } } diff --git a/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/LoginMethodDataSource.kt b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/LoginMethodDataSource.kt new file mode 100644 index 00000000..b21df56a --- /dev/null +++ b/core/datastore/api/src/main/kotlin/com/ninecraft/booket/core/datastore/api/datasource/LoginMethodDataSource.kt @@ -0,0 +1,10 @@ +package com.ninecraft.booket.core.datastore.api.datasource + +import com.ninecraft.booket.core.model.LoginMethod +import kotlinx.coroutines.flow.Flow + +interface LoginMethodDataSource { + val recentLoginMethod: Flow + suspend fun setRecentLoginMethod(loginMethod: LoginMethod) + suspend fun clearRecentLoginMethod() +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLoginMethodDataSource.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLoginMethodDataSource.kt new file mode 100644 index 00000000..edb601d2 --- /dev/null +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/datasource/DefaultLoginMethodDataSource.kt @@ -0,0 +1,48 @@ +package com.ninecraft.booket.core.datastore.impl.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.ninecraft.booket.core.datastore.api.datasource.LoginMethodDataSource +import com.ninecraft.booket.core.datastore.impl.di.LoginMethodDataStore +import com.ninecraft.booket.core.datastore.impl.util.handleIOException +import com.ninecraft.booket.core.di.DataScope +import com.ninecraft.booket.core.model.LoginMethod +import dev.zacsweers.metro.Inject +import dev.zacsweers.metro.SingleIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@SingleIn(DataScope::class) +@Inject +class DefaultLoginMethodDataSource( + @LoginMethodDataStore private val dataStore: DataStore, +) : LoginMethodDataSource { + override val recentLoginMethod: Flow = dataStore.data + .handleIOException() + .map { prefs -> + val method = prefs[RECENT_LOGIN_METHOD] + when (method) { + "KAKAO" -> LoginMethod.KAKAO + "GOOGLE" -> LoginMethod.GOOGLE + else -> LoginMethod.NONE + } + } + + override suspend fun setRecentLoginMethod(loginMethod: LoginMethod) { + dataStore.edit { prefs -> + prefs[RECENT_LOGIN_METHOD] = loginMethod.name + } + } + + override suspend fun clearRecentLoginMethod() { + dataStore.edit { prefs -> + prefs.remove(RECENT_LOGIN_METHOD) + } + } + + companion object { + private val RECENT_LOGIN_METHOD = stringPreferencesKey("RECENT_LOGIN_METHOD") + } +} diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreGraph.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreGraph.kt index a052bb43..b390b5ab 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreGraph.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreGraph.kt @@ -6,11 +6,13 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore import com.ninecraft.booket.core.datastore.api.datasource.BookRecentSearchDataSource import com.ninecraft.booket.core.datastore.api.datasource.LibraryRecentSearchDataSource +import com.ninecraft.booket.core.datastore.api.datasource.LoginMethodDataSource import com.ninecraft.booket.core.datastore.api.datasource.NotificationDataSource import com.ninecraft.booket.core.datastore.api.datasource.OnboardingDataSource import com.ninecraft.booket.core.datastore.api.datasource.TokenDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultBookRecentSearchDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultLibraryRecentSearchDataSource +import com.ninecraft.booket.core.datastore.impl.datasource.DefaultLoginMethodDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultNotificationDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultOnboardingDataSource import com.ninecraft.booket.core.datastore.impl.datasource.DefaultTokenDataSource @@ -25,12 +27,14 @@ private const val BOOK_RECENT_SEARCH_DATASTORE_NAME = "BOOK_RECENT_SEARCH_DATAST private const val LIBRARY_RECENT_SEARCH_DATASTORE_NAME = "LIBRARY_RECENT_SEARCH_DATASTORE" private const val ONBOARDING_DATASTORE_NAME = "ONBOARDING_DATASTORE" private const val NOTIFICATION_DATASTORE_NAME = "NOTIFICATION_DATASTORE" +private const val LOGIN_METHOD_DATASTORE_NAME = "LOGIN_METHOD_DATASTORE" private val Context.tokenDataStore by preferencesDataStore(name = TOKEN_DATASTORE_NAME) private val Context.bookRecentSearchDataStore by preferencesDataStore(name = BOOK_RECENT_SEARCH_DATASTORE_NAME) private val Context.libraryRecentSearchDataStore by preferencesDataStore(name = LIBRARY_RECENT_SEARCH_DATASTORE_NAME) private val Context.onboardingDataStore by preferencesDataStore(name = ONBOARDING_DATASTORE_NAME) private val Context.notificationDataStore by preferencesDataStore(name = NOTIFICATION_DATASTORE_NAME) +private val Context.loginMethodDataStore by preferencesDataStore(name = LOGIN_METHOD_DATASTORE_NAME) @ContributesTo(DataScope::class) interface DataStoreGraph { @@ -65,6 +69,12 @@ interface DataStoreGraph { @ApplicationContext context: Context, ): DataStore = context.notificationDataStore + @LoginMethodDataStore + @Provides + fun provideLoginMethodDataStore( + @ApplicationContext context: Context, + ): DataStore = context.loginMethodDataStore + @Binds val DefaultTokenDataSource.bind: TokenDataSource @@ -79,4 +89,7 @@ interface DataStoreGraph { @Binds val DefaultNotificationDataSource.bind: NotificationDataSource + + @Binds + val DefaultLoginMethodDataSource.bind: LoginMethodDataSource } diff --git a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt index 9dd43f0c..4f404203 100644 --- a/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt +++ b/core/datastore/impl/src/main/kotlin/com/ninecraft/booket/core/datastore/impl/di/DataStoreQualifier.kt @@ -21,3 +21,7 @@ annotation class OnboardingDataStore @Qualifier @Retention(AnnotationRetention.BINARY) annotation class NotificationDataStore + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class LoginMethodDataStore diff --git a/core/designsystem/stability/designsystem.stability b/core/designsystem/stability/designsystem.stability index a44f1681..ed4f6c9e 100644 --- a/core/designsystem/stability/designsystem.stability +++ b/core/designsystem/stability/designsystem.stability @@ -163,39 +163,6 @@ public fun com.ninecraft.booket.core.designsystem.component.checkbox.TickOnlyChe - onCheckedChange: STABLE (function type) - modifier: STABLE (marked @Stable or @Immutable) -@Composable -public fun com.ninecraft.booket.core.designsystem.component.chip.ReedRemovableChip(label: kotlin.String, chipSizeStyle: com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle, onRemove: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true - restartable: true - params: - - label: STABLE (String is immutable) - - chipSizeStyle: STABLE (class with no mutable properties) - - onRemove: STABLE (function type) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -public fun com.ninecraft.booket.core.designsystem.component.chip.ReedSelectableChip(label: kotlin.String, chipSizeStyle: com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle, selected: kotlin.Boolean, onClick: kotlin.Function0, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: true - restartable: true - params: - - label: STABLE (String is immutable) - - chipSizeStyle: STABLE (class with no mutable properties) - - selected: STABLE (primitive type) - - onClick: STABLE (function type) - - modifier: STABLE (marked @Stable or @Immutable) - -@Composable -public fun com.ninecraft.booket.core.designsystem.component.chip.mediumChipStyle(): com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle - skippable: true - restartable: true - params: - -@Composable -public fun com.ninecraft.booket.core.designsystem.component.chip.smallChipStyle(): com.ninecraft.booket.core.designsystem.component.chip.ChipSizeStyle - skippable: true - restartable: true - params: - @Composable public fun com.ninecraft.booket.core.designsystem.component.textfield.ReedRecordTextField(recordState: androidx.compose.foundation.text.input.TextFieldState, recordHintRes: kotlin.Int, modifier: androidx.compose.ui.Modifier, inputTransformation: androidx.compose.foundation.text.input.InputTransformation?, keyboardOptions: androidx.compose.foundation.text.KeyboardOptions, lineLimits: androidx.compose.foundation.text.input.TextFieldLineLimits, isError: kotlin.Boolean, errorMessage: kotlin.String, onClear: kotlin.Function0?, onNext: kotlin.Function0, backgroundColor: androidx.compose.ui.graphics.Color, textColor: androidx.compose.ui.graphics.Color, cornerShape: androidx.compose.foundation.shape.RoundedCornerShape, borderStroke: androidx.compose.foundation.BorderStroke): kotlin.Unit skippable: true @@ -232,3 +199,40 @@ public fun com.ninecraft.booket.core.designsystem.component.textfield.ReedTextFi - borderStroke: STABLE (marked @Stable or @Immutable) - searchIconTint: STABLE (marked @Stable or @Immutable) +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme(content: @[Composable] androidx.compose.runtime.internal.ComposableFunction0): kotlin.Unit + skippable: true + restartable: true + params: + - content: STABLE (composable function type) + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.border(): com.ninecraft.booket.core.designsystem.theme.ReedBorder + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.colors(): com.ninecraft.booket.core.designsystem.theme.ReedColorScheme + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.radius(): com.ninecraft.booket.core.designsystem.theme.ReedRadius + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.spacing(): com.ninecraft.booket.core.designsystem.theme.ReedSpacing + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.core.designsystem.theme.ReedTheme.typography(): com.ninecraft.booket.core.designsystem.theme.ReedTypography + skippable: true + restartable: true + params: + diff --git a/core/model/src/main/kotlin/com/ninecraft/booket/core/model/LoginMethod.kt b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/LoginMethod.kt new file mode 100644 index 00000000..dd7f9445 --- /dev/null +++ b/core/model/src/main/kotlin/com/ninecraft/booket/core/model/LoginMethod.kt @@ -0,0 +1,7 @@ +package com.ninecraft.booket.core.model + +enum class LoginMethod { + NONE, + KAKAO, + GOOGLE, +} diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt index 9a7a21a0..d18b8da2 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginPresenter.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.feature.login import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope @@ -10,6 +11,7 @@ import com.ninecraft.booket.core.common.constants.ErrorScope import com.ninecraft.booket.core.common.event.postErrorDialog import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.model.LoginMethod import com.ninecraft.booket.feature.screens.HomeScreen import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.TermsAgreementScreen @@ -50,6 +52,15 @@ class LoginPresenter( val scope = rememberCoroutineScope() var isLoading by rememberRetained { mutableStateOf(false) } var sideEffect by rememberRetained { mutableStateOf(null) } + var showLoginTooltip by rememberRetained { mutableStateOf(false) } + var recentLoginMethod by rememberRetained { mutableStateOf(LoginMethod.NONE) } + + LaunchedEffect(Unit) { + authRepository.recentLoginMethod.collect { method -> + recentLoginMethod = method + showLoginTooltip = method != LoginMethod.NONE + } + } fun navigateAfterLogin() { scope.launch { @@ -102,6 +113,13 @@ class LoginPresenter( isLoading = true authRepository.login(event.providerType, event.token) .onSuccess { + authRepository.setRecentLoginMethod( + if (event.providerType == LoginUiEvent.PROVIDER_TYPE_KAKAO) { + LoginMethod.KAKAO + } else { + LoginMethod.GOOGLE + }, + ) userRepository.syncFcmToken() navigateAfterLogin() }.onFailure { exception -> @@ -125,6 +143,10 @@ class LoginPresenter( is LoginUiEvent.OnCloseButtonClick -> { navigator.pop() } + + is LoginUiEvent.OnDismissLoginTooltip -> { + showLoginTooltip = false + } } } @@ -136,6 +158,8 @@ class LoginPresenter( isLoading = isLoading, returnToScreen = screen.returnToScreen, sideEffect = sideEffect, + showLoginTooltip = showLoginTooltip, + recentLoginMethod = recentLoginMethod, eventSink = ::handleEvent, ) } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt index 82cd656c..7c520323 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUi.kt @@ -2,13 +2,13 @@ package com.ninecraft.booket.feature.login import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -20,7 +20,9 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.common.extensions.noRippleClickable import com.ninecraft.booket.core.designsystem.DevicePreview import com.ninecraft.booket.core.designsystem.component.button.ReedButton import com.ninecraft.booket.core.designsystem.component.button.ReedButtonColorStyle @@ -29,9 +31,11 @@ import com.ninecraft.booket.core.designsystem.component.button.largeButtonStyle import com.ninecraft.booket.core.designsystem.component.button.smallButtonStyle import com.ninecraft.booket.core.designsystem.theme.ReedTheme import com.ninecraft.booket.core.designsystem.theme.White +import com.ninecraft.booket.core.model.LoginMethod import com.ninecraft.booket.core.ui.ReedScaffold import com.ninecraft.booket.core.ui.component.ReedCloseTopAppBar import com.ninecraft.booket.core.ui.component.ReedLoadingIndicator +import com.ninecraft.booket.feature.login.component.LoginTooltipBox import com.ninecraft.booket.feature.screens.LoginScreen import com.skydoves.compose.stability.runtime.TraceRecomposition import com.slack.circuit.codegen.annotations.CircuitInject @@ -52,109 +56,145 @@ internal fun LoginUi( ReedScaffold( modifier = modifier.fillMaxSize(), ) { innerPadding -> - Column( - modifier = modifier + Box( + modifier = Modifier .fillMaxSize() .background(White) - .padding(innerPadding), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + .padding(innerPadding) + .then( + if (state.showLoginTooltip) { + Modifier.noRippleClickable { + state.eventSink(LoginUiEvent.OnDismissLoginTooltip) + } + } else { + Modifier + }, + ), ) { - Box(modifier = Modifier.fillMaxSize()) { - Column { - if (state.returnToScreen != null) { - ReedCloseTopAppBar( - onClose = { - state.eventSink(LoginUiEvent.OnCloseButtonClick) - }, - ) - } - Column( + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (state.returnToScreen != null) { + ReedCloseTopAppBar( + onClose = { + state.eventSink(LoginUiEvent.OnCloseButtonClick) + }, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.img_reed_logo_big), + contentDescription = "Reed Logo", + modifier = Modifier.height(67.14.dp), + ) + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) + Text( + text = stringResource(R.string.login_reed_slogan), + color = ReedTheme.colors.contentBrand, + style = ReedTheme.typography.headline2SemiBold, + ) + Spacer(modifier = Modifier.weight(1f)) + + // 카카오 로그인 버튼 + 툴팁 + Box( + modifier = Modifier.fillMaxWidth(), + ) { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.KAKAO, modifier = Modifier .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(R.drawable.img_reed_logo_big), - contentDescription = "Reed Logo", - modifier = Modifier.height(67.14.dp), - ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing5)) - Text( - text = stringResource(R.string.login_reed_slogan), - color = ReedTheme.colors.contentBrand, - style = ReedTheme.typography.headline2SemiBold, - ) - } - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnKakaoLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.KAKAO, + .padding(horizontal = ReedTheme.spacing.spacing5), + text = stringResource(id = R.string.kakao_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), + contentDescription = "Kakao Icon", + tint = Color.Unspecified, + ) + }, + ) + + if (state.showLoginTooltip && state.recentLoginMethod == LoginMethod.KAKAO) { + LoginTooltipBox( + messageResId = R.string.recent_login, modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = R.string.kakao_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_kakao), - contentDescription = "Kakao Icon", - tint = Color.Unspecified, - ) - }, + .align(Alignment.BottomEnd) + .offset { + IntOffset( + x = (-28).dp.roundToPx(), + y = (-32).dp.roundToPx(), + ) + }, ) - Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) - ReedButton( - onClick = { - state.eventSink(LoginUiEvent.OnGoogleLoginButtonClick) - }, - sizeStyle = largeButtonStyle, - colorStyle = ReedButtonColorStyle.GOOGLE, + } + } + + Spacer(modifier = Modifier.height(ReedTheme.spacing.spacing2)) + + // 구글 로그인 버튼 + 툴팁 + Box( + modifier = Modifier.fillMaxWidth(), + ) { + ReedButton( + onClick = { + state.eventSink(LoginUiEvent.OnGoogleLoginButtonClick) + }, + sizeStyle = largeButtonStyle, + colorStyle = ReedButtonColorStyle.GOOGLE, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = ReedTheme.spacing.spacing5), + text = stringResource(id = R.string.google_login), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_google), + contentDescription = "Google Icon", + tint = Color.Unspecified, + ) + }, + ) + + if (state.showLoginTooltip && state.recentLoginMethod == LoginMethod.GOOGLE) { + LoginTooltipBox( + messageResId = R.string.recent_login, modifier = Modifier - .fillMaxWidth() - .padding( - start = ReedTheme.spacing.spacing5, - end = ReedTheme.spacing.spacing5, - ), - text = stringResource(id = R.string.google_login), - leadingIcon = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_google), - contentDescription = "Google Icon", - tint = Color.Unspecified, - ) - }, - ) - Spacer( - modifier = Modifier.height(if (state.returnToScreen == null) ReedTheme.spacing.spacing3 else ReedTheme.spacing.spacing8), - ) - if (state.returnToScreen == null) { - ReedTextButton( - onClick = { - state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) + .align(Alignment.BottomEnd) + .offset { + IntOffset( + x = (-28).dp.roundToPx(), + y = (-32).dp.roundToPx(), + ) }, - text = stringResource(R.string.guest_login), - sizeStyle = smallButtonStyle, - colorStyle = ReedButtonColorStyle.TEXT, - ) - } + ) } } - if (state.isLoading) { - ReedLoadingIndicator() + Spacer( + modifier = Modifier.height( + if (state.returnToScreen == null) ReedTheme.spacing.spacing3 else ReedTheme.spacing.spacing8, + ), + ) + + if (state.returnToScreen == null) { + ReedTextButton( + onClick = { + state.eventSink(LoginUiEvent.OnGuestLoginButtonClick) + }, + text = stringResource(R.string.guest_login), + sizeStyle = smallButtonStyle, + colorStyle = ReedButtonColorStyle.TEXT, + ) } } + + if (state.isLoading) { + ReedLoadingIndicator() + } } } } diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt index 8c49aea8..b4f6a676 100644 --- a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/LoginUiState.kt @@ -1,6 +1,7 @@ package com.ninecraft.booket.feature.login import androidx.compose.runtime.Immutable +import com.ninecraft.booket.core.model.LoginMethod import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen @@ -10,6 +11,8 @@ data class LoginUiState( val isLoading: Boolean = false, val returnToScreen: Screen? = null, val sideEffect: LoginSideEffect? = null, + val showLoginTooltip: Boolean = false, + val recentLoginMethod: LoginMethod = LoginMethod.NONE, val eventSink: (LoginUiEvent) -> Unit, ) : CircuitUiState @@ -33,6 +36,7 @@ sealed interface LoginUiEvent : CircuitUiEvent { data class LoginFailure(val message: String) : LoginUiEvent data object OnGuestLoginButtonClick : LoginUiEvent data object OnCloseButtonClick : LoginUiEvent + data object OnDismissLoginTooltip : LoginUiEvent companion object { const val PROVIDER_TYPE_KAKAO = "KAKAO" diff --git a/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/component/LoginTooltipBox.kt b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/component/LoginTooltipBox.kt new file mode 100644 index 00000000..dfe09878 --- /dev/null +++ b/feature/login/src/main/kotlin/com/ninecraft/booket/feature/login/component/LoginTooltipBox.kt @@ -0,0 +1,99 @@ +package com.ninecraft.booket.feature.login.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.feature.login.R + +private val TriangleShape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + val path = Path().apply { + // 왼쪽 위 + moveTo(0f, 0f) + // 오른쪽 위 + lineTo(size.width, 0f) + // 중앙 아래 (뾰족한 부분) + lineTo(size.width / 2, size.height) + // 닫기 + close() + } + return Outline.Generic(path) + } +} + +@Composable +internal fun LoginTooltipBox( + @StringRes messageResId: Int, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Box( + modifier = Modifier + .shadow(ReedTheme.radius.xs, RoundedCornerShape(ReedTheme.radius.xs), clip = false) + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(ReedTheme.colors.contentBrand) + .padding( + horizontal = ReedTheme.spacing.spacing3, + vertical = ReedTheme.spacing.spacing2, + ), + ) { + Text( + text = stringResource(messageResId), + color = ReedTheme.colors.contentInverse, + style = ReedTheme.typography.label2Regular, + ) + } + Box( + Modifier + .width(ReedTheme.spacing.spacing3) + .height(ReedTheme.spacing.spacing3 / 2) + .offset { + IntOffset( + x = 14.dp.roundToPx(), + y = 0, + ) + } + .graphicsLayer { + shadowElevation = 8.dp.toPx() + shape = TriangleShape + clip = true + } + .background(ReedTheme.colors.contentBrand), + ) + } +} + +@ComponentPreview +@Composable +private fun LoginTooltipBoxPreview() { + ReedTheme { + LoginTooltipBox(messageResId = R.string.recent_login) + } +} diff --git a/feature/login/src/main/res/values/strings.xml b/feature/login/src/main/res/values/strings.xml index 32c3f405..dc136219 100644 --- a/feature/login/src/main/res/values/strings.xml +++ b/feature/login/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ 약관 전체 동의 시작하기 회원가입 없이 둘러보기 + 최근 로그인 (필수)서비스 이용약관 (필수)개인정보처리방침 diff --git a/feature/login/stability/login.stability b/feature/login/stability/login.stability index 58cad8b4..4afc8410 100644 --- a/feature/login/stability/login.stability +++ b/feature/login/stability/login.stability @@ -20,6 +20,20 @@ internal fun com.ninecraft.booket.feature.login.LoginUi(state: com.ninecraft.boo - state: UNSTABLE (has mutable properties or unstable members) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +internal fun com.ninecraft.booket.feature.login.component.LoginTooltipBox(messageResId: kotlin.Int, modifier: androidx.compose.ui.Modifier): kotlin.Unit + skippable: true + restartable: true + params: + - messageResId: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) + +@Composable +private fun com.ninecraft.booket.feature.login.component.LoginTooltipBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable internal fun com.ninecraft.booket.feature.termsagreement.HandleTermsAgreementSideEffects(state: com.ninecraft.booket.feature.termsagreement.TermsAgreementUiState): kotlin.Unit skippable: false diff --git a/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/RecordTooltipBox.kt b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/RecordTooltipBox.kt new file mode 100644 index 00000000..fb70bdff --- /dev/null +++ b/feature/record/src/main/kotlin/com/ninecraft/booket/feature/record/component/RecordTooltipBox.kt @@ -0,0 +1,72 @@ +package com.ninecraft.booket.feature.record.component + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.ninecraft.booket.core.designsystem.ComponentPreview +import com.ninecraft.booket.core.designsystem.theme.ReedTheme +import com.ninecraft.booket.feature.record.R + +@Composable +internal fun RecordTooltipBox( + @StringRes messageResId: Int, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + Modifier + .shadow(ReedTheme.radius.xs, RoundedCornerShape(ReedTheme.radius.xs), clip = false) + .clip(RoundedCornerShape(ReedTheme.radius.xs)) + .background(ReedTheme.colors.contentPrimary) + .padding( + horizontal = ReedTheme.spacing.spacing3, + vertical = ReedTheme.spacing.spacing2, + ), + ) { + Text( + text = stringResource(messageResId), + color = ReedTheme.colors.contentInverse, + style = ReedTheme.typography.label2Regular, + ) + } + Box( + Modifier + .padding(start = 2.dp) + .size(ReedTheme.spacing.spacing3) + .offset { + IntOffset( + x = (-10).dp.roundToPx(), + y = 0, + ) + } + .graphicsLayer { + rotationZ = 45f + shadowElevation = 8.dp.toPx() + clip = true + } + .background(ReedTheme.colors.contentPrimary), + ) + } +} + +@ComponentPreview +@Composable +private fun RecordTooltipBoxPreview() { + ReedTheme { + RecordTooltipBox(messageResId = R.string.scan_tooltip_message) + } +} diff --git a/feature/record/stability/record.stability b/feature/record/stability/record.stability index 953962fc..41cde6e5 100644 --- a/feature/record/stability/record.stability +++ b/feature/record/stability/record.stability @@ -4,35 +4,103 @@ // Do not edit this file directly. To update it, run: // ./gradlew :record:stabilityDump +@Composable +public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheet(onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, impressionState: androidx.compose.foundation.text.input.TextFieldState, impressionGuideList: kotlinx.collections.immutable.ImmutableList, beforeSelectedImpressionGuide: kotlin.String, selectedImpressionGuide: kotlin.String, onGuideClick: kotlin.Function1, onCloseButtonClick: kotlin.Function0, onSelectionConfirmButtonClick: kotlin.Function0): kotlin.Unit + skippable: true + restartable: true + params: + - onDismissRequest: STABLE (function type) + - sheetState: STABLE (marked @Stable or @Immutable) + - impressionState: STABLE (marked @Stable or @Immutable) + - impressionGuideList: STABLE (known stable type) + - beforeSelectedImpressionGuide: STABLE (String is immutable) + - selectedImpressionGuide: STABLE (String is immutable) + - onGuideClick: STABLE (function type) + - onCloseButtonClick: STABLE (function type) + - onSelectionConfirmButtonClick: STABLE (function type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.ImpressionGuideBottomSheetPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +public fun com.ninecraft.booket.feature.record.component.ImpressionGuideBox(onClick: kotlin.Function0, impressionText: kotlin.String, modifier: androidx.compose.ui.Modifier, isSelected: kotlin.Boolean): kotlin.Unit + skippable: true + restartable: true + params: + - onClick: STABLE (function type) + - impressionText: STABLE (String is immutable) + - modifier: STABLE (marked @Stable or @Immutable) + - isSelected: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.ImpressionGuideBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +internal fun com.ninecraft.booket.feature.record.component.RecordTooltipBox(messageResId: kotlin.Int): kotlin.Unit + skippable: true + restartable: true + params: + - messageResId: STABLE (primitive type) + +@Composable +private fun com.ninecraft.booket.feature.record.component.RecordTooltipBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable private fun com.ninecraft.booket.feature.record.ocr.CameraPreview(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: false + skippable: true restartable: true params: - - state: UNSTABLE (has mutable properties or unstable members) + - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.record.ocr.CameraPreviewPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable internal fun com.ninecraft.booket.feature.record.ocr.HandleOcrSideEffects(state: com.ninecraft.booket.feature.record.ocr.OcrUiState): kotlin.Unit - skippable: false + skippable: true + restartable: true + params: + - state: STABLE (class with no mutable properties) + +@Composable +public fun com.ninecraft.booket.feature.record.ocr.OcrPresenter.present(): com.ninecraft.booket.feature.record.ocr.OcrUiState + skippable: true restartable: true params: - - state: UNSTABLE (has mutable properties or unstable members) @Composable internal fun com.ninecraft.booket.feature.record.ocr.OcrUi(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: false + skippable: true restartable: true params: - - state: UNSTABLE (has mutable properties or unstable members) + - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.record.ocr.TextRecognitionResultPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable private fun com.ninecraft.booket.feature.record.ocr.TextScanResult(state: com.ninecraft.booket.feature.record.ocr.OcrUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit - skippable: false + skippable: true restartable: true params: - - state: UNSTABLE (has mutable properties or unstable members) + - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) @Composable @@ -42,6 +110,12 @@ public fun com.ninecraft.booket.feature.record.ocr.component.CameraFrame(modifie params: - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.record.ocr.component.CameraFramePreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable public fun com.ninecraft.booket.feature.record.ocr.component.SentenceBox(onClick: kotlin.Function0, sentence: kotlin.String, modifier: androidx.compose.ui.Modifier, isSelected: kotlin.Boolean): kotlin.Unit skippable: true @@ -52,6 +126,12 @@ public fun com.ninecraft.booket.feature.record.ocr.component.SentenceBox(onClick - modifier: STABLE (marked @Stable or @Immutable) - isSelected: STABLE (primitive type) +@Composable +private fun com.ninecraft.booket.feature.record.ocr.component.SentenceBoxPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable internal fun com.ninecraft.booket.feature.record.register.HandleRecordRegisterSideEffects(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState): kotlin.Unit skippable: true @@ -65,6 +145,12 @@ public fun com.ninecraft.booket.feature.record.register.RecordRegisterPresenter. restartable: true params: +@Composable +private fun com.ninecraft.booket.feature.record.register.RecordRegisterPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable internal fun com.ninecraft.booket.feature.record.register.RecordRegisterUi(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true @@ -74,39 +160,37 @@ internal fun com.ninecraft.booket.feature.record.register.RecordRegisterUi(state - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.record.step.EmotionDetailBottomSheet(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onDismissRequest: kotlin.Function0, sheetState: androidx.compose.material3.SheetState, onCloseButtonClick: kotlin.Function0, onEmotionDetailToggled: kotlin.Function1, onSkipButtonClick: kotlin.Function0, onConfirmButtonClick: kotlin.Function0): kotlin.Unit +private fun com.ninecraft.booket.feature.record.step.EmotionItem(emotion: com.ninecraft.booket.core.model.Emotion, onClick: kotlin.Function0, isSelected: kotlin.Boolean, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - emotionGroup: STABLE (marked @Stable or @Immutable) - - selectedEmotionDetailIds: STABLE (known stable type) - - onDismissRequest: STABLE (function type) - - sheetState: STABLE (marked @Stable or @Immutable) - - onCloseButtonClick: STABLE (function type) - - onEmotionDetailToggled: STABLE (function type) - - onSkipButtonClick: STABLE (function type) - - onConfirmButtonClick: STABLE (function type) + - emotion: STABLE (class with no mutable properties) + - onClick: STABLE (function type) + - isSelected: STABLE (primitive type) + - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.record.step.EmotionItem(emotionGroup: com.ninecraft.booket.core.model.EmotionGroupModel, selectedEmotionDetailIds: kotlinx.collections.immutable.ImmutableList, onClick: kotlin.Function0, isSelected: kotlin.Boolean, onEmotionDetailRemove: kotlin.Function1, modifier: androidx.compose.ui.Modifier): kotlin.Unit +public fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - - emotionGroup: STABLE (marked @Stable or @Immutable) - - selectedEmotionDetailIds: STABLE (known stable type) - - onClick: STABLE (function type) - - isSelected: STABLE (primitive type) - - onEmotionDetailRemove: STABLE (function type) + - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) @Composable -internal fun com.ninecraft.booket.feature.record.step.EmotionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit +public fun com.ninecraft.booket.feature.record.step.ImpressionStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true restartable: true params: - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.record.step.ImpressionStepPreview(): kotlin.Unit + skippable: true + restartable: true + params: + @Composable internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninecraft.booket.feature.record.register.RecordRegisterUiState, modifier: androidx.compose.ui.Modifier): kotlin.Unit skippable: true @@ -115,3 +199,15 @@ internal fun com.ninecraft.booket.feature.record.step.QuoteStep(state: com.ninec - state: STABLE (class with no mutable properties) - modifier: STABLE (marked @Stable or @Immutable) +@Composable +private fun com.ninecraft.booket.feature.record.step.QuoteStepPreview(): kotlin.Unit + skippable: true + restartable: true + params: + +@Composable +private fun com.ninecraft.booket.feature.record.step.RecordRegisterPreview(): kotlin.Unit + skippable: true + restartable: true + params: + diff --git a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt index 47de5d50..39cdc8a2 100644 --- a/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt +++ b/feature/settings/src/main/kotlin/com/ninecraft/booket/feature/settings/SettingsPresenter.kt @@ -11,6 +11,7 @@ import com.ninecraft.booket.core.common.utils.handleException import com.ninecraft.booket.core.data.api.repository.AuthRepository import com.ninecraft.booket.core.data.api.repository.RemoteConfigRepository import com.ninecraft.booket.core.data.api.repository.UserRepository +import com.ninecraft.booket.core.model.LoginMethod import com.ninecraft.booket.core.model.state.UserState import com.ninecraft.booket.feature.screens.LoginScreen import com.ninecraft.booket.feature.screens.NotificationScreen @@ -102,6 +103,7 @@ class SettingsPresenter( authRepository.withdraw() .onSuccess { userRepository.resetNotificationData() + authRepository.setRecentLoginMethod(LoginMethod.NONE) analyticsHelper.logEvent(SETTINGS_WITHDRAWAL_COMPLETE) navigator.resetRoot(LoginScreen()) }