diff --git a/common/src/main/java/com/yapp/common/util/Keyboard.kt b/common/src/main/java/com/yapp/common/util/Keyboard.kt new file mode 100644 index 00000000..a65de52a --- /dev/null +++ b/common/src/main/java/com/yapp/common/util/Keyboard.kt @@ -0,0 +1,40 @@ +package com.yapp.common.util + +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import androidx.compose.ui.platform.LocalView + +private const val KeyBoardVisibleThreshold = 0.15f + +/** + * 키보드의 Show/Hide 상태를 가져온다. + * @return 키보드의 visible 상태 + */ +@Composable +fun rememberKeyboardVisible( + initialKeyboardState: Boolean = false, +): State { + val view = LocalView.current + return produceState(initialValue = initialKeyboardState) { + val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + value = view.isKeyboardOpen() + } + view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) + awaitDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + } + } +} + +private fun View.isKeyboardOpen(): Boolean { + val rect = Rect().also { + getWindowVisibleDisplayFrame(it) + } + val screenHeight = rootView.height + val keypadHeight = screenHeight - rect.bottom + return keypadHeight > screenHeight * KeyBoardVisibleThreshold +} \ No newline at end of file diff --git a/common/src/main/java/com/yapp/common/util/KeyboardVisibilityUtils.kt b/common/src/main/java/com/yapp/common/util/KeyboardVisibilityUtils.kt deleted file mode 100644 index cbb19a55..00000000 --- a/common/src/main/java/com/yapp/common/util/KeyboardVisibilityUtils.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.yapp.common.util - -import android.graphics.Rect -import android.view.ViewTreeObserver -import android.view.Window - -class KeyboardVisibilityUtils( - private val window: Window, - private val onShowKeyboard: () -> Unit, - private val onHideKeyboard: () -> Unit -) { - private val windowVisibleDisplayFrame = Rect() - private var lastVisibleDecorViewHeight: Int = Int.MAX_VALUE - private val MIN_KEYBOARD_HEIGHT_PX = 100 - - private val onGlobalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { - window.decorView.getWindowVisibleDisplayFrame(windowVisibleDisplayFrame) - val visibleDecorViewHeight = windowVisibleDisplayFrame.height() - - if (lastVisibleDecorViewHeight != 0) { - if (lastVisibleDecorViewHeight > visibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX) { - onShowKeyboard.invoke() - } else if (lastVisibleDecorViewHeight + MIN_KEYBOARD_HEIGHT_PX < visibleDecorViewHeight) { - onHideKeyboard.invoke() - } - } - - lastVisibleDecorViewHeight = visibleDecorViewHeight - } - - init { - window.decorView.viewTreeObserver.addOnGlobalLayoutListener(onGlobalLayoutListener) - } - - fun detachKeyboardListener() { - window.decorView.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalLayoutListener) - } - -} \ No newline at end of file diff --git a/common/src/main/res/drawable/illust_password_1.xml b/common/src/main/res/drawable/illust_password_1.xml new file mode 100644 index 00000000..63bf3ac1 --- /dev/null +++ b/common/src/main/res/drawable/illust_password_1.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/drawable/illust_password_2.xml b/common/src/main/res/drawable/illust_password_2.xml new file mode 100644 index 00000000..9a96bc86 --- /dev/null +++ b/common/src/main/res/drawable/illust_password_2.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/drawable/illust_password_3.xml b/common/src/main/res/drawable/illust_password_3.xml new file mode 100644 index 00000000..fadb40f4 --- /dev/null +++ b/common/src/main/res/drawable/illust_password_3.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common/src/main/res/drawable/illust_password_4.xml b/common/src/main/res/drawable/illust_password_4.xml new file mode 100644 index 00000000..77d4248b --- /dev/null +++ b/common/src/main/res/drawable/illust_password_4.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSource.kt b/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSource.kt index 61924f62..7d59be3a 100644 --- a/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSource.kt +++ b/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSource.kt @@ -12,4 +12,5 @@ interface FirebaseRemoteConfigDataSource { suspend fun getTeamList(): List suspend fun getQrPassword(): String suspend fun shouldShowGuestButton(): Boolean + suspend fun getSignUpPassword(): String } \ No newline at end of file diff --git a/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSourceImpl.kt b/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSourceImpl.kt index 6c297064..a3ecae1e 100644 --- a/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSourceImpl.kt +++ b/data/src/main/java/com/yapp/data/datasource/FirebaseRemoteConfigDataSourceImpl.kt @@ -107,4 +107,15 @@ class FirebaseRemoteConfigDataSourceImpl @Inject constructor() : FirebaseRemoteC } } + override suspend fun getSignUpPassword(): String { + return suspendCancellableCoroutine { cancellableContinuation -> + firebaseRemoteConfig.fetchAndActivate().addOnSuccessListener { + val password = firebaseRemoteConfig.getString(RemoteConfigData.SignUpPassword.key) + cancellableContinuation.resume(password, null) + }.addOnFailureListener { exception -> + cancellableContinuation.resumeWithException(exception) + } + } + } + } \ No newline at end of file diff --git a/data/src/main/java/com/yapp/data/repository/RemoteConfigRepositoryImpl.kt b/data/src/main/java/com/yapp/data/repository/RemoteConfigRepositoryImpl.kt index 26c9d309..131a9671 100644 --- a/data/src/main/java/com/yapp/data/repository/RemoteConfigRepositoryImpl.kt +++ b/data/src/main/java/com/yapp/data/repository/RemoteConfigRepositoryImpl.kt @@ -94,4 +94,16 @@ class RemoteConfigRepositoryImpl @Inject constructor( ) } + override suspend fun getSignUpPassword(): Result { + return runCatching { + firebaseRemoteConfigDataSource.getSignUpPassword() + }.fold( + onSuccess = { signUpPassword: String -> + Result.success(signUpPassword) + }, + onFailure = { exception -> + Result.failure(exception) + } + ) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/yapp/domain/firebase/RemoteConfigData.kt b/domain/src/main/java/com/yapp/domain/firebase/RemoteConfigData.kt index 769ee236..4149d4b3 100644 --- a/domain/src/main/java/com/yapp/domain/firebase/RemoteConfigData.kt +++ b/domain/src/main/java/com/yapp/domain/firebase/RemoteConfigData.kt @@ -34,6 +34,11 @@ sealed class RemoteConfigData { override val defaultValue: String = "fail" } + object SignUpPassword : RemoteConfigData() { + override val key: String = SIGN_UP_PASSWORD + override val defaultValue: String = "" + } + companion object { private const val ATTENDANCE_MAGINOTLINE_TIME = "attendance_maginotline_time" private const val ATTENDANCE_SESSION_LIST = "attendance_session_list" @@ -41,6 +46,7 @@ sealed class RemoteConfigData { private const val ATTENDANCE_CONFIG = "config" private const val ATTENDANCE_QR_PASSWORD = "attendance_qr_password" private const val SHOULD_SHOW_GUEST_BUTTON = "should_show_guest_button" + private const val SIGN_UP_PASSWORD = "attendance_signup_password" val defaultMaps = mapOf( MaginotlineTime.defaultValue to MaginotlineTime.key, diff --git a/domain/src/main/java/com/yapp/domain/repository/RemoteConfigRepository.kt b/domain/src/main/java/com/yapp/domain/repository/RemoteConfigRepository.kt index b624ec35..83544165 100644 --- a/domain/src/main/java/com/yapp/domain/repository/RemoteConfigRepository.kt +++ b/domain/src/main/java/com/yapp/domain/repository/RemoteConfigRepository.kt @@ -12,4 +12,5 @@ interface RemoteConfigRepository { suspend fun getTeamList(): Result> suspend fun getQrPassword(): Result suspend fun shouldShowGuestButton(): Result + suspend fun getSignUpPassword(): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/yapp/domain/usecases/CheckSignUpPasswordUseCase.kt b/domain/src/main/java/com/yapp/domain/usecases/CheckSignUpPasswordUseCase.kt new file mode 100644 index 00000000..692187b3 --- /dev/null +++ b/domain/src/main/java/com/yapp/domain/usecases/CheckSignUpPasswordUseCase.kt @@ -0,0 +1,14 @@ +package com.yapp.domain.usecases + +import com.yapp.domain.repository.RemoteConfigRepository +import javax.inject.Inject + +class CheckSignUpPasswordUseCase @Inject constructor( + private val remoteConfigRepository: RemoteConfigRepository, +) { + suspend operator fun invoke(inputPassword: String): Result { + return remoteConfigRepository.getSignUpPassword().mapCatching { qrPassword: String -> + inputPassword == qrPassword + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/yapp/presentation/ui/AttendanceScreen.kt b/presentation/src/main/java/com/yapp/presentation/ui/AttendanceScreen.kt index 0ec28847..c9f48708 100644 --- a/presentation/src/main/java/com/yapp/presentation/ui/AttendanceScreen.kt +++ b/presentation/src/main/java/com/yapp/presentation/ui/AttendanceScreen.kt @@ -8,7 +8,14 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,6 +46,7 @@ import com.yapp.presentation.ui.member.qrcodescanner.QrCodeScanner import com.yapp.presentation.ui.member.score.detail.SessionDetail import com.yapp.presentation.ui.member.setting.MemberSetting import com.yapp.presentation.ui.member.signup.name.Name +import com.yapp.presentation.ui.member.signup.password.Password import com.yapp.presentation.ui.member.signup.position.Position import com.yapp.presentation.ui.member.signup.team.Team import com.yapp.presentation.ui.splash.Splash @@ -84,7 +92,7 @@ fun AttendanceScreen( } }, navigateToSignUpScreen = { - navController.navigate(AttendanceScreenRoute.SIGNUP_NAME.route) { + navController.navigate(AttendanceScreenRoute.SIGNUP_PASSWORD.route) { popUpTo(AttendanceScreenRoute.LOGIN.route) { inclusive = true } } }, @@ -235,6 +243,23 @@ fun AttendanceScreen( SessionDetail { navController.popBackStack() } } + composable( + route = AttendanceScreenRoute.SIGNUP_PASSWORD.route, + ) { + SetStatusBarColorByRoute(it.destination.route) + Password( + onClickBackButton = { + navController.navigate(AttendanceScreenRoute.LOGIN.route) { + popUpTo(AttendanceScreenRoute.SIGNUP_PASSWORD.route) { inclusive = true } + } + }, + onClickNextButton = { + navController.navigate(AttendanceScreenRoute.SIGNUP_NAME.route) { + popUpTo(AttendanceScreenRoute.SIGNUP_PASSWORD.route) { inclusive = true } + } + }) + } + composable( route = AttendanceScreenRoute.SIGNUP_NAME.route ) { @@ -321,6 +346,7 @@ enum class AttendanceScreenRoute(val route: String) { SIGNUP_NAME("signup-name"), SIGNUP_POSITION("signup-position"), SIGNUP_TEAM("signup-team"), + SIGNUP_PASSWORD("signup-password"), HELP("help"), ADMIN_TOTAL_SCORE("admin-total-score"), SESSION_DETAIL("session-detail"), diff --git a/presentation/src/main/java/com/yapp/presentation/ui/member/signup/name/Name.kt b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/name/Name.kt index f4002477..2ae29ed3 100644 --- a/presentation/src/main/java/com/yapp/presentation/ui/member/signup/name/Name.kt +++ b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/name/Name.kt @@ -1,31 +1,52 @@ package com.yapp.presentation.ui.member.signup.name -import android.app.Activity import androidx.activity.compose.BackHandler +import androidx.compose.animation.Crossfade import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.google.accompanist.insets.navigationBarsWithImePadding import com.google.accompanist.insets.statusBarsPadding -import com.yapp.common.theme.* -import com.yapp.common.util.KeyboardVisibilityUtils +import com.yapp.common.theme.AttendanceTheme +import com.yapp.common.theme.AttendanceTypography +import com.yapp.common.util.rememberKeyboardVisible import com.yapp.common.yds.YDSAppBar import com.yapp.common.yds.YDSButtonLarge import com.yapp.common.yds.YDSPopupDialog import com.yapp.common.yds.YdsButtonState import com.yapp.presentation.R import com.yapp.presentation.ui.member.signup.name.NameContract.NameSideEffect -import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.* +import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.InputName +import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.OnBackButtonClick +import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.OnCancelButtonClick +import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.OnDismissDialogButtonClick +import com.yapp.presentation.ui.member.signup.name.NameContract.NameUiEvent.OnNextButtonClick import kotlinx.coroutines.flow.collectLatest @OptIn(ExperimentalComposeUiApi::class) @@ -36,12 +57,7 @@ fun Name( onClickNextBtn: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsState() - var isKeyboardOpened by remember { mutableStateOf(false) } - val keyboardVisibilityUtils = KeyboardVisibilityUtils( - window = (LocalContext.current as Activity).window, - onShowKeyboard = { isKeyboardOpened = true }, - onHideKeyboard = { isKeyboardOpened = false } - ) + val isKeyboardVisible by rememberKeyboardVisible() val onCancelButtonClick by remember { mutableStateOf({ viewModel.setEvent(OnCancelButtonClick) }) } val onNextButtonClick: () -> Unit by remember { @@ -101,10 +117,9 @@ fun Name( NextButton( enabled = uiState.name.isNotBlank(), - isKeyboardOpened = isKeyboardOpened, + isKeyboardVisible = isKeyboardVisible, modifier = Modifier.align(Alignment.BottomCenter), onClickNextBtn = { onNextButtonClick() }, - keyboardVisibilityUtils = keyboardVisibilityUtils ) } } @@ -155,7 +170,10 @@ fun InputName(name: String, onInputName: (String) -> Unit) { singleLine = true, modifier = Modifier .fillMaxWidth() - .background(color = AttendanceTheme.colors.grayScale.Gray200, shape = RoundedCornerShape(50.dp)), + .background( + color = AttendanceTheme.colors.grayScale.Gray200, + shape = RoundedCornerShape(50.dp) + ), placeholder = { Text( text = stringResource(id = R.string.name_example_hint), @@ -178,19 +196,17 @@ fun InputName(name: String, onInputName: (String) -> Unit) { @Composable fun NextButton( enabled: Boolean, - isKeyboardOpened: Boolean, + isKeyboardVisible: Boolean, modifier: Modifier, onClickNextBtn: () -> Unit, - keyboardVisibilityUtils: KeyboardVisibilityUtils ) { Box( modifier = modifier, ) { - if (isKeyboardOpened) { + if (isKeyboardVisible) { OnKeyboardNextButton( enabled = enabled, onClickNextBtn = onClickNextBtn, - keyboardVisibilityUtils = keyboardVisibilityUtils ) } else { YDSButtonLarge( @@ -199,7 +215,6 @@ fun NextButton( onClick = { if (enabled) { onClickNextBtn() - keyboardVisibilityUtils.detachKeyboardListener() } }, modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 40.dp) @@ -211,18 +226,17 @@ fun NextButton( @Composable fun OnKeyboardNextButton( + modifier: Modifier = Modifier, enabled: Boolean, onClickNextBtn: () -> Unit, - keyboardVisibilityUtils: KeyboardVisibilityUtils ) { Button( enabled = enabled, contentPadding = PaddingValues(0.dp), onClick = { onClickNextBtn() - keyboardVisibilityUtils.detachKeyboardListener() }, - modifier = Modifier + modifier = modifier .fillMaxWidth() .height(60.dp), shape = RoundedCornerShape(0.dp), diff --git a/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PassWordViewModel.kt b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PassWordViewModel.kt new file mode 100644 index 00000000..eb683759 --- /dev/null +++ b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PassWordViewModel.kt @@ -0,0 +1,66 @@ +package com.yapp.presentation.ui.member.signup.password + +import androidx.lifecycle.viewModelScope +import com.google.firebase.FirebaseNetworkException +import com.yapp.common.base.BaseViewModel +import com.yapp.domain.usecases.CheckSignUpPasswordUseCase +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordSideEffect +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordUiEvent +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordUiState +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordUiState.Companion.PasswordDigit +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class PassWordViewModel @Inject constructor( + private val checkSignUpPasswordUseCase: CheckSignUpPasswordUseCase, +) : BaseViewModel(PasswordUiState()) { + + override suspend fun handleEvent(event: PasswordUiEvent) { + when (event) { + is PasswordUiEvent.InputPassword -> { + updatePassword(password = event.password) + } + + PasswordUiEvent.OnBackButtonClick -> { + setEffect(PasswordSideEffect.NavigateToPreviousScreen) + } + + PasswordUiEvent.OnNextButtonClick -> { + checkPassword(password = currentState.inputPassword) + } + } + } + + private fun updatePassword(password: String) { + if (password.length <= PasswordDigit) { + setState { copy(inputPassword = password, isWrong = false) } + } + } + + private fun checkPassword(password: String) = viewModelScope.launch { + setEffect(PasswordSideEffect.KeyboardHide) + checkSignUpPasswordUseCase(password) + .onSuccess { isPasswordValid -> + when (isPasswordValid) { + true -> { + setEffect(PasswordSideEffect.NavigateToNextScreen) + } + + false -> { + setState { + copy( + isWrong = true, + ) + } + } + } + }.onFailure { exception -> + if (exception is FirebaseNetworkException) { + setEffect(PasswordSideEffect.ShowToast("네트워크 연결을 확인해주세요")) + } + } + } + +} diff --git a/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/Password.kt b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/Password.kt new file mode 100644 index 00000000..d08963de --- /dev/null +++ b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/Password.kt @@ -0,0 +1,244 @@ +@file:OptIn(ExperimentalComposeUiApi::class) + +package com.yapp.presentation.ui.member.signup.password + +import android.widget.Toast +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.NonRestartableComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.insets.navigationBarsWithImePadding +import com.google.accompanist.insets.statusBarsPadding +import com.yapp.common.flow.collectAsStateWithLifecycle +import com.yapp.common.theme.AttendanceTheme +import com.yapp.common.theme.AttendanceTypography +import com.yapp.common.theme.Light_Gray_200 +import com.yapp.common.util.rememberKeyboardVisible +import com.yapp.common.yds.YDSAppBar +import com.yapp.presentation.R +import com.yapp.presentation.ui.member.signup.name.OnKeyboardNextButton +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordSideEffect +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordUiEvent +import com.yapp.presentation.ui.member.signup.password.PasswordContract.PasswordUiState.Companion.PasswordDigit +import kotlinx.coroutines.flow.collectLatest + +@Composable +internal fun Password( + viewModel: PassWordViewModel = hiltViewModel(), + onClickBackButton: () -> Unit, + onClickNextButton: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val keyboardVisible by rememberKeyboardVisible() + val focusRequester = remember { FocusRequester() } + val context = LocalContext.current + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + LaunchedEffect(key1 = viewModel.effect) { + viewModel.effect.collectLatest { + when (it) { + is PasswordSideEffect.ShowToast -> { + Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show() + } + + PasswordSideEffect.KeyboardHide -> { + keyboardController?.hide() + } + + PasswordSideEffect.NavigateToNextScreen -> { + onClickNextButton() + } + + PasswordSideEffect.NavigateToPreviousScreen -> { + onClickBackButton() + } + } + } + } + + Scaffold( + topBar = { + YDSAppBar( + modifier = Modifier.background(AttendanceTheme.colors.backgroundColors.background), + onClickBackButton = { + viewModel.setEvent(PasswordUiEvent.OnBackButtonClick) + }, + ) + }, + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsWithImePadding() + ) { contentPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .background(AttendanceTheme.colors.backgroundColors.background) + .padding(contentPadding) + ) { + Column( + modifier = Modifier.padding(horizontal = 24.dp) + ) { + Spacer(modifier = Modifier.padding(top = 40.dp)) + Text( + text = stringResource(id = R.string.member_signup_password_title), + color = AttendanceTheme.colors.grayScale.Gray1200, + style = AttendanceTypography.h1 + ) + Spacer(modifier = Modifier.padding(top = 12.dp)) + Text( + text = stringResource(id = R.string.member_signup_password_subtitle), + color = AttendanceTheme.colors.grayScale.Gray800, + style = AttendanceTypography.body1 + ) + Spacer(modifier = Modifier.padding(top = 28.dp)) + PasswordTextField( + modifier = Modifier + .wrapContentSize() + .focusRequester(focusRequester), + value = uiState.inputPassword, + onValueChange = { + viewModel.setEvent(PasswordUiEvent.InputPassword(it)) + }, + keyboardOptions = KeyboardOptions().copy( + keyboardType = KeyboardType.NumberPassword, + ), + ) + Spacer(modifier = Modifier.padding(top = 20.dp)) + AnimatedVisibility( + visible = uiState.isWrong, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = stringResource(id = R.string.member_signup_password_wrong_code), + color = AttendanceTheme.colors.etcColors.EtcRed, + style = AttendanceTypography.subtitle1, + ) + } + } + } + if (keyboardVisible) { + OnKeyboardNextButton( + modifier = Modifier.align(Alignment.BottomCenter), + enabled = uiState.inputPassword.length == PasswordDigit, + onClickNextBtn = { + viewModel.setEvent(PasswordUiEvent.OnNextButtonClick) + }, + ) + } + } + } +} + +@Composable +internal fun PasswordTextField( + modifier: Modifier = Modifier, + value: String, + onValueChange: (String) -> Unit, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + BasicTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + decorationBox = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Absolute.SpaceBetween, + ) { + value.forEachIndexed { index, _ -> + PasswordBox( + resId = getPasswordImageByIndex(index) + ) + } + repeat(PasswordDigit - value.length) { + GrayBox() + } + } + }, + ) +} + +private fun getPasswordImageByIndex(index: Int): Int { + return when (index) { + 0 -> com.yapp.common.R.drawable.illust_password_1 + 1 -> com.yapp.common.R.drawable.illust_password_2 + 2 -> com.yapp.common.R.drawable.illust_password_3 + else -> com.yapp.common.R.drawable.illust_password_4 + } +} + +@Composable +@NonRestartableComposable +private fun PasswordBox( + modifier: Modifier = Modifier, + @DrawableRes resId: Int, +) { + GrayBox(modifier = modifier) { + Image( + modifier = Modifier.fillMaxSize(), + painter = painterResource(id = resId), + contentDescription = null, + ) + } +} + +@Composable +@NonRestartableComposable +private fun GrayBox( + modifier: Modifier = Modifier, + content: (@Composable () -> Unit)? = null, +) { + Box( + modifier = modifier + .size(72.dp) + .clip(CircleShape) + .background(Light_Gray_200), + ) { + content?.invoke() + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PasswordContract.kt b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PasswordContract.kt new file mode 100644 index 00000000..fb504173 --- /dev/null +++ b/presentation/src/main/java/com/yapp/presentation/ui/member/signup/password/PasswordContract.kt @@ -0,0 +1,32 @@ +package com.yapp.presentation.ui.member.signup.password + +import com.yapp.common.base.UiEvent +import com.yapp.common.base.UiSideEffect +import com.yapp.common.base.UiState + +class PasswordContract { + data class PasswordUiState( + val isError: Boolean = false, + val correctPassword: String = "", + val inputPassword: String = "", + val nextButtonEnabled: Boolean = false, + val isWrong: Boolean = false, + ) : UiState { + companion object { + const val PasswordDigit = 4 + } + } + + sealed class PasswordSideEffect : UiSideEffect { + object NavigateToPreviousScreen : PasswordSideEffect() + object NavigateToNextScreen : PasswordSideEffect() + object KeyboardHide : PasswordSideEffect() + class ShowToast(val message: String) : PasswordSideEffect() + } + + sealed class PasswordUiEvent : UiEvent { + data class InputPassword(val password: String) : PasswordUiEvent() + object OnNextButtonClick : PasswordUiEvent() + object OnBackButtonClick : PasswordUiEvent() + } +} diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 8931ff25..a510b536 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -31,7 +31,11 @@ 관리 20기 누적 출결 점수 누적 점수 확인하기 - + + 너, 내 동료가 돼라! + 암호 코드 4자리를 입력해주세요 + 틀린 코드입니다 + 속한 직군을\n알려주세요 YAPP 시작하기