diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 247e18374..14776ca16 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -150,6 +150,7 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.startup) implementation(libs.swipe.refresh.layout) + debugImplementation(libs.compose.ui.tooling) implementation(libs.inappupdate) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0533c2d0f..5f00b1dca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,7 +146,6 @@ android:name=".feature.fortune.FortuneActivity" android:exported="true" android:launchMode="singleTop" /> - diff --git a/app/src/main/java/org/sopt/official/feature/auth/AuthActivity.kt b/app/src/main/java/org/sopt/official/feature/auth/AuthActivity.kt index 54f300ec1..5caef6796 100644 --- a/app/src/main/java/org/sopt/official/feature/auth/AuthActivity.kt +++ b/app/src/main/java/org/sopt/official/feature/auth/AuthActivity.kt @@ -24,24 +24,54 @@ */ package org.sopt.official.feature.auth -import android.animation.ObjectAnimator import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.Intent -import android.graphics.Paint import android.os.Bundle -import android.view.animation.AnimationUtils +import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseIn +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.core.app.NotificationCompat -import androidx.core.view.isVisible +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.sopt.official.BuildConfig import org.sopt.official.R @@ -51,18 +81,20 @@ import org.sopt.official.auth.impl.api.AuthService import org.sopt.official.auth.impl.model.request.AuthRequest import org.sopt.official.auth.model.UserStatus import org.sopt.official.common.di.Auth -import org.sopt.official.common.util.dp -import org.sopt.official.common.util.setOnAnimationEndListener -import org.sopt.official.common.util.setOnSingleClickListener -import org.sopt.official.common.util.viewBinding -import org.sopt.official.databinding.ActivityAuthBinding +import org.sopt.official.designsystem.Gray300 +import org.sopt.official.designsystem.Gray700 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.designsystem.White +import org.sopt.official.feature.auth.component.AuthButton +import org.sopt.official.feature.auth.component.AuthTextWithArrow +import org.sopt.official.feature.auth.component.LoginErrorDialog import org.sopt.official.feature.home.HomeActivity import org.sopt.official.network.model.response.OAuthToken import org.sopt.official.network.persistence.SoptDataStore +import javax.inject.Inject @AndroidEntryPoint class AuthActivity : AppCompatActivity() { - private val binding by viewBinding(ActivityAuthBinding::inflate) private val viewModel by viewModels() @Auth @@ -74,106 +106,226 @@ class AuthActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (dataStore.accessToken.isNotEmpty()) { - startActivity( - HomeActivity.getIntent(this, HomeActivity.StartArgs(UserStatus.of(dataStore.userStatus))) - ) - } - setContentView(binding.root) - initNotificationChannel() + setContent { + SoptTheme { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current - initUi() - initAnimation() - collectUiEvent() - } + val action by viewModel.loginDialogAction.collectAsStateWithLifecycle() - private fun initNotificationChannel() { - NotificationChannel( - getString(R.string.toolbar_notification), - getString(R.string.toolbar_notification), - NotificationManager.IMPORTANCE_HIGH - ).apply { - setSound(null, null) - enableLights(false) - enableVibration(false) - lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC - (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(this) - } - } + LaunchedEffect(true) { + if (dataStore.accessToken.isNotEmpty()) { + startActivity( + HomeActivity.getIntent(context, HomeActivity.StartArgs(UserStatus.of(dataStore.userStatus))) + ) + } + } - private fun collectUiEvent() { - viewModel.uiEvent - .flowWithLifecycle(lifecycle) - .onEach { event -> - when (event) { - is AuthUiEvent.Success -> startActivity( - HomeActivity.getIntent(this, HomeActivity.StartArgs(event.userStatus)) - ) + LaunchedEffect(true) { + NotificationChannel( + getString(R.string.toolbar_notification), + getString(R.string.toolbar_notification), + NotificationManager.IMPORTANCE_HIGH + ).apply { + setSound(null, null) + enableLights(false) + enableVibration(false) + lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC + (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(this) + } + } + + LaunchedEffect(viewModel.uiEvent, lifecycleOwner) { + viewModel.uiEvent.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) + .collect { event -> + when (event) { + is AuthUiEvent.Success -> startActivity( + HomeActivity.getIntent(context, HomeActivity.StartArgs(event.userStatus)) + ) + + is AuthUiEvent.Failure -> startActivity( + HomeActivity.getIntent(context, HomeActivity.StartArgs(UserStatus.UNAUTHENTICATED)) + ) + } + } + } - is AuthUiEvent.Failure -> startActivity( - HomeActivity.getIntent(this, HomeActivity.StartArgs(UserStatus.UNAUTHENTICATED)) + if (action == true) { + LoginErrorDialog( + onDismissRequest = { viewModel.showLoginErrorDialog(false) } ) } - }.launchIn(lifecycleScope) + + AuthScreen( + showDialog = { viewModel.showLoginErrorDialog(true) }, + onGoogleLoginCLick = { + PlaygroundAuth.authorizeWithWebTab( + context = context, + isDebug = BuildConfig.DEBUG, + authDataSource = object : PlaygroundAuthDatasource { + override suspend fun oauth(code: String): Result { + return kotlin.runCatching { + authService + .authenticate(AuthRequest(code, dataStore.pushToken)) + .toOAuthToken() + } + } + } + ) { + it.onSuccess { token -> + lifecycleScope.launch { + viewModel.onLogin(token.toEntity()) + } + }.onFailure { + lifecycleScope.launch { + viewModel.onFailure(it) + } + } + } + }, + onLoginLaterClick = { + startActivity( + HomeActivity.getIntent( + this, + HomeActivity.StartArgs( + UserStatus.UNAUTHENTICATED + ) + ) + ) + } + ) + } + } } - private fun initAnimation() { - val fadeInAnimation = AnimationUtils.loadAnimation(this, R.anim.anim_fade_in).apply { - startOffset = 700 + @Composable + fun AuthScreen( + showDialog: () -> Unit, + onGoogleLoginCLick: () -> Unit, + onLoginLaterClick: () -> Unit + ) { + var showAuthBottom by remember { mutableStateOf(false) } + val offsetY = remember { Animatable(0f) } + + LaunchedEffect(Unit) { + delay(700) + showAuthBottom = true + offsetY.animateTo( + targetValue = -140f, + animationSpec = tween( + durationMillis = 1500, + easing = FastOutSlowInEasing + ) + ) } - fadeInAnimation.setOnAnimationEndListener { - binding.groupBottomAuth.isVisible = true + + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(id = R.drawable.img_logo), + contentDescription = "솝트 로고", + modifier = Modifier.offset(y = offsetY.value.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + } + AnimatedVisibility( + visible = showAuthBottom, + enter = fadeIn( + initialAlpha = 0.1f, + animationSpec = tween(easing = EaseIn) + ), + modifier = Modifier.align(Alignment.BottomCenter) + ) { + AuthBottom( + showDialog = showDialog, + onGoogleLoginCLick = onGoogleLoginCLick, + onLoginLaterClick = onLoginLaterClick + ) + } } - ObjectAnimator.ofFloat( - binding.imgSoptLogo, - "translationY", - -140.dp.toFloat() - ).apply { - duration = 1000 - startDelay = 700 - interpolator = AnimationUtils.loadInterpolator( - this@AuthActivity, - android.R.interpolator.fast_out_slow_in - ) - }.start() - binding.groupBottomAuth.startAnimation(fadeInAnimation) } - private fun initUi() { - binding.btnSoptNotMember.paintFlags = Paint.UNDERLINE_TEXT_FLAG - binding.btnSoptLogin.setOnSingleClickListener { - PlaygroundAuth.authorizeWithWebTab( - context = this, - isDebug = BuildConfig.DEBUG, - authDataSource = object : PlaygroundAuthDatasource { - override suspend fun oauth(code: String): Result { - return kotlin.runCatching { - authService - .authenticate(AuthRequest(code, dataStore.pushToken)) - .toOAuthToken() - } - } - } + @Composable + private fun AuthBottom( + showDialog: () -> Unit, + onGoogleLoginCLick: () -> Unit, + onLoginLaterClick: () -> Unit + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + AuthButton( + padding = PaddingValues(vertical = 12.dp), + onClick = onGoogleLoginCLick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) ) { - it.onSuccess { token -> - lifecycleScope.launch { - viewModel.onLogin(token.toEntity()) - } - }.onFailure { - lifecycleScope.launch { - viewModel.onFailure(it) - } - } + Image( + painter = painterResource(id = R.drawable.ic_auth_google), + contentDescription = "구글 로고", + modifier = Modifier.padding(end = 4.dp) + ) + Text( + text = "Google로 로그인", + style = SoptTheme.typography.label16SB + ) } - } - binding.btnSoptNotMember.setOnSingleClickListener { - startActivity( - HomeActivity.getIntent( - this, - HomeActivity.StartArgs( - UserStatus.UNAUTHENTICATED - ) + Spacer(modifier = Modifier.height(16.dp)) + AuthTextWithArrow( + text = "로그인이 안 되나요?", + modifier = Modifier.clickable(onClick = showDialog) + ) + Spacer(modifier = Modifier.height(44.dp)) + AuthDivider() + Spacer(modifier = Modifier.height(16.dp)) + AuthButton( + padding = PaddingValues(vertical = 12.dp), + onClick = { + // TODO SOPT 회원가입 기능 구현 by leeeyubin + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + containerColor = Gray700, + contentColor = White + ) { + Text( + text = "SOPT 회원가입", + style = SoptTheme.typography.label16SB ) + } + Spacer(modifier = Modifier.height(16.dp)) + AuthTextWithArrow( + text = "나중에 로그인할래요.", + modifier = Modifier.clickable(onClick = onLoginLaterClick) + ) + Spacer(modifier = Modifier.height(28.dp)) + } + } + + @Composable + private fun AuthDivider() { + Row( + modifier = Modifier.padding(horizontal = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + HorizontalDivider( + thickness = Dp.Hairline, + color = Gray300, + modifier = Modifier.weight(1f) + ) + Text( + text = "또는", + color = Gray300, + modifier = Modifier.padding(horizontal = 8.dp) + ) + HorizontalDivider( + thickness = Dp.Hairline, + color = Gray300, + modifier = Modifier.weight(1f) ) } } @@ -184,4 +336,16 @@ class AuthActivity : AppCompatActivity() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } } + + @Preview(showBackground = true) + @Composable + private fun AuthScreenPreview() { + SoptTheme { + AuthScreen( + showDialog = {}, + onGoogleLoginCLick = {}, + onLoginLaterClick = {} + ) + } + } } diff --git a/app/src/main/java/org/sopt/official/feature/auth/AuthViewModel.kt b/app/src/main/java/org/sopt/official/feature/auth/AuthViewModel.kt index 7d9720a38..fffc226d6 100644 --- a/app/src/main/java/org/sopt/official/feature/auth/AuthViewModel.kt +++ b/app/src/main/java/org/sopt/official/feature/auth/AuthViewModel.kt @@ -26,13 +26,17 @@ package org.sopt.official.feature.auth import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import org.sopt.official.auth.model.Auth import org.sopt.official.auth.model.UserStatus import org.sopt.official.domain.usecase.LoginUseCase import timber.log.Timber +import javax.inject.Inject sealed interface AuthUiEvent { data class Success(val userStatus: UserStatus) : AuthUiEvent @@ -45,6 +49,10 @@ class AuthViewModel @Inject constructor( ) : ViewModel() { private val _uiEvent = MutableSharedFlow() val uiEvent = _uiEvent.asSharedFlow() + + private val _loginDialogAction = MutableStateFlow(null) + val loginDialogAction: StateFlow = _loginDialogAction.asStateFlow() + suspend fun onLogin(auth: Auth) { loginUseCase(auth) _uiEvent.emit(AuthUiEvent.Success(auth.status)) @@ -54,4 +62,8 @@ class AuthViewModel @Inject constructor( Timber.e(throwable) _uiEvent.emit(AuthUiEvent.Failure(throwable.message ?: "로그인에 실패했습니다.")) } + + fun showLoginErrorDialog(visible: Boolean) { + _loginDialogAction.update { visible } + } } diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/AuthButton.kt b/app/src/main/java/org/sopt/official/feature/auth/component/AuthButton.kt new file mode 100644 index 000000000..42300e973 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/AuthButton.kt @@ -0,0 +1,69 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.Black +import org.sopt.official.designsystem.Black80 +import org.sopt.official.designsystem.Gray60 +import org.sopt.official.designsystem.White + +@Composable +internal fun AuthButton( + padding: PaddingValues, + onClick: () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(10.dp), + containerColor: Color = White, + contentColor: Color = Black, + isEnabled: Boolean = true, + disabledContainerColor: Color = Black80, + disabledContentColor: Color = Gray60, + content: @Composable () -> Unit, +) { + Button( + onClick = onClick, + enabled = isEnabled, + contentPadding = padding, + shape = shape, + modifier = modifier, + colors = ButtonDefaults.buttonColors( + containerColor = containerColor, + contentColor = contentColor, + disabledContainerColor = disabledContainerColor, + disabledContentColor = disabledContentColor + ) + ) { + content() + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextField.kt b/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextField.kt new file mode 100644 index 000000000..a3f07b378 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextField.kt @@ -0,0 +1,75 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.Black60 +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.Gray100 +import org.sopt.official.designsystem.SoptTheme + +@Composable +internal fun AuthTextField( + labelText: String, + hintText: String, + onTextChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + var isFocused by remember { mutableStateOf(false) } + + BasicTextField( + value = labelText, + onValueChange = onTextChange, + modifier = modifier + .background(color = Black60, shape = RoundedCornerShape(10.dp)) + .onFocusChanged { focusState -> + isFocused = focusState.isFocused + } + .padding(vertical = 15.dp, horizontal = 20.dp), + textStyle = SoptTheme.typography.body14M.copy(color = Gray10), + decorationBox = { innerTextField -> + if (labelText.isEmpty()) { + Text( + text = hintText, + color = Gray100, + style = SoptTheme.typography.body14M + ) + } + innerTextField() + } + ) +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextWithArrow.kt b/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextWithArrow.kt new file mode 100644 index 000000000..db528b1df --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/AuthTextWithArrow.kt @@ -0,0 +1,75 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import org.sopt.official.R +import org.sopt.official.designsystem.Gray30 +import org.sopt.official.designsystem.SoptTheme + +@Composable +internal fun AuthTextWithArrow( + text: String, + modifier: Modifier = Modifier, + textStyle: TextStyle = SoptTheme.typography.label14SB, + textColor: Color = Gray30, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + color = textColor, + style = textStyle + ) + Icon( + painter = painterResource(R.drawable.ic_auth_arrow_right), + contentDescription = "화살표 버튼", + tint = Gray30 + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun AuthTextWithButtonPreview() { + SoptTheme { + AuthTextWithArrow( + text = "text", + textColor = Gray30, + textStyle = SoptTheme.typography.label12SB + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/CertificationSnackBar.kt b/app/src/main/java/org/sopt/official/feature/auth/component/CertificationSnackBar.kt new file mode 100644 index 000000000..98cc2c006 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/CertificationSnackBar.kt @@ -0,0 +1,80 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.SoptTheme + +@Composable +internal fun CertificationSnackBar( + message: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(18.dp)) + .background(Gray10) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_auth_certification_notice), + contentDescription = "인증 확인 아이콘" + ) + Text( + text = message, + style = SoptTheme.typography.title14SB + ) + } +} + +@Composable +@Preview(showBackground = true) +private fun AuthSnackBarPreview() { + SoptTheme { + CertificationSnackBar( + message = "인증번호가 전송되었어요." + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/LoginErrorDialog.kt b/app/src/main/java/org/sopt/official/feature/auth/component/LoginErrorDialog.kt new file mode 100644 index 000000000..f4ffdc6d9 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/LoginErrorDialog.kt @@ -0,0 +1,154 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import org.sopt.official.R +import org.sopt.official.designsystem.Black +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.Gray700 +import org.sopt.official.designsystem.Gray800 +import org.sopt.official.designsystem.SoptTheme + +@Composable +internal fun LoginErrorDialog( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + Popup { + Box( + modifier = Modifier + .fillMaxSize() + .background(Black.copy(alpha = 0.5f)) + .clickable(onClick = onDismissRequest) + ) { + Column( + modifier = modifier + .align(Alignment.BottomCenter) + .padding(20.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Gray800) + .padding(8.dp) + ) { + DialogTitle() + Spacer(modifier = Modifier.height(12.dp)) + LoginDialogText( + text = "로그인한 계정을 알고 싶어요.", + onClick = {} + ) + Spacer(modifier = Modifier.height(4.dp)) + LoginDialogText( + text = "소셜 계정을 재설정하고 싶어요.", + onClick = {} + ) + Spacer(modifier = Modifier.height(4.dp)) + LoginDialogText( + text = "카카오톡 채널에 문의할게요.", + onClick = {} + ) + } + } + } +} + +@Composable +private fun DialogTitle() { + Row( + modifier = Modifier.padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_auth_alert_circle), + contentDescription = "로그인 에러 아이콘", + tint = Gray10, + modifier = Modifier.padding(start = 4.dp) + ) + Text( + text = "로그인이 안 되나요?", + color = Gray10, + style = SoptTheme.typography.title20SB + ) + } +} + +@Composable +private fun LoginDialogText( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val backgroundColor = if (isPressed) Gray700 else Gray800 + + Text( + text = text, + color = Gray10, + modifier = modifier + .clickable(interactionSource = interactionSource, indication = null) { + onClick() + } + .clip(RoundedCornerShape(8.dp)) + .background(backgroundColor) + .fillMaxWidth() + .padding(10.dp), + style = SoptTheme.typography.body16R, + ) +} + +@Preview(showBackground = true) +@Composable +private fun LoginErrorDialogPreview() { + SoptTheme { + LoginErrorDialog( + onDismissRequest = {} + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/component/PhoneCertification.kt b/app/src/main/java/org/sopt/official/feature/auth/component/PhoneCertification.kt new file mode 100644 index 000000000..9a4546293 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/component/PhoneCertification.kt @@ -0,0 +1,107 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.Gray80 +import org.sopt.official.designsystem.Gray950 +import org.sopt.official.designsystem.SoptTheme + +enum class CertificationButtonText(val message: String) { + GET_CODE("인증번호 요청"), + CHANGE_CODE("재전송하기") +} + +@Composable +internal fun PhoneCertification( + onPhoneNumberClick: () -> Unit, + textColor: Color, + modifier: Modifier = Modifier, + phoneNumber: String = "", +) { + var buttonState by remember { mutableStateOf(CertificationButtonText.GET_CODE) } + + Column(modifier = modifier) { + Text( + text = "전화번호", + color = textColor, + style = SoptTheme.typography.body14M + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(7.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AuthTextField( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + labelText = phoneNumber, + hintText = "010-XXXX-XXXX", + onTextChange = {} + ) + AuthButton( + padding = PaddingValues(vertical = 16.dp, horizontal = 14.dp), + onClick = onPhoneNumberClick, + containerColor = Gray10, + contentColor = Gray950, + ) { + Text( + text = buttonState.message, + style = SoptTheme.typography.body14M + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PhoneCertificationPreview() { + SoptTheme { + PhoneCertification( + onPhoneNumberClick = {}, + textColor = Gray80 + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/feature/autherror/AuthErrorScreen.kt b/app/src/main/java/org/sopt/official/feature/auth/feature/autherror/AuthErrorScreen.kt new file mode 100644 index 000000000..1e1667fdd --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/feature/autherror/AuthErrorScreen.kt @@ -0,0 +1,108 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.feature.autherror + +import androidx.compose.foundation.Image +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.Orange400 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.designsystem.White +import org.sopt.official.feature.auth.component.AuthButton +import org.sopt.official.feature.auth.component.AuthTextWithArrow + +@Composable +fun AuthErrorScreen( + loginAgain: () -> Unit = {} +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.ic_auth_error), + contentDescription = "로그인 오류 이미지" + ) + Text( + text = buildAnnotatedString { + append("앗! ") + withStyle(style = SpanStyle(color = Orange400)) { + append("회원 정보") + } + append("를 찾을 수 없어요.\n") + withStyle(style = SoptTheme.typography.title18SB.toSpanStyle()) { + append("먼저 회원가입 후, 다시 로그인해주세요.") + } + }, + color = White, + style = SoptTheme.typography.title24SB, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.weight(1f)) + AuthButton( + onClick = loginAgain, + modifier = Modifier + .padding(horizontal = 20.dp) + .fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + padding = PaddingValues(vertical = 16.dp), + ) { + Text( + text = "다시 로그인하기", + style = SoptTheme.typography.label18SB + ) + } + Spacer(modifier = Modifier.height(16.dp)) + AuthTextWithArrow(text = "로그인이 안 되나요?") + Spacer(modifier = Modifier.height(28.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun AuthErrorPreview() { + SoptTheme { + AuthErrorScreen() + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/feature/certificatemember/CertificateMemberScreen.kt b/app/src/main/java/org/sopt/official/feature/auth/feature/certificatemember/CertificateMemberScreen.kt new file mode 100644 index 000000000..21a030c07 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/feature/certificatemember/CertificateMemberScreen.kt @@ -0,0 +1,233 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.feature.certificatemember + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import org.sopt.official.R.drawable.ic_auth_certification_error +import org.sopt.official.R.drawable.ic_auth_memeber_error +import org.sopt.official.R.drawable.ic_auth_process +import org.sopt.official.designsystem.Blue500 +import org.sopt.official.designsystem.BlueAlpha100 +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.Gray100 +import org.sopt.official.designsystem.Gray60 +import org.sopt.official.designsystem.Gray80 +import org.sopt.official.designsystem.Gray950 +import org.sopt.official.designsystem.Red100 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.designsystem.White +import org.sopt.official.feature.auth.component.AuthButton +import org.sopt.official.feature.auth.component.AuthTextField +import org.sopt.official.feature.auth.component.AuthTextWithArrow +import org.sopt.official.feature.auth.component.CertificationSnackBar +import org.sopt.official.feature.auth.component.PhoneCertification + +@Composable +fun CertificateMemberScreen() { + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val onShowSnackBar: () -> Unit = remember { + { + coroutineScope.launch { + snackBarHostState.showSnackbar("인증번호가 전송되었어요.") + } + } + } + + SnackbarHost( + hostState = snackBarHostState, + modifier = Modifier + .padding(top = 16.dp) + .padding(horizontal = 16.dp), + snackbar = { message -> + CertificationSnackBar(message = message.visuals.message) + } + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(72.dp)) + TopBar() + Spacer(modifier = Modifier.height(44.dp)) + PhoneCertification( + onPhoneNumberClick = onShowSnackBar, + textColor = Gray80 + ) + Spacer(modifier = Modifier.height(10.dp)) + AuthTextField( + modifier = Modifier.fillMaxWidth(), + labelText = "", + hintText = "인증번호를 입력해 주세요.", + onTextChange = {} + ) + Spacer(modifier = Modifier.height(41.dp)) + AuthButton( + modifier = Modifier + .fillMaxWidth() + .border( + color = Blue500, + width = 1.dp, + shape = RoundedCornerShape(10.dp) + ), + padding = PaddingValues(vertical = 14.dp, horizontal = 18.dp), + onClick = {}, + containerColor = BlueAlpha100, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Image( + painterResource(id = ic_auth_memeber_error), + contentDescription = "에러 아이콘", + ) + Column { + AuthTextWithArrow( + text = "SOPT 회원 인증에 실패하셨나요?", + textStyle = SoptTheme.typography.body14M + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = "번호가 바뀌었거나, 인증이 어려우신 경우 추가 정보 인증을 통해 가입을 도와드리고 있어요!", + color = Gray60 + ) + } + } + } + Spacer(modifier = Modifier.weight(1f)) + AuthButton( + modifier = Modifier.fillMaxWidth(), + padding = PaddingValues(vertical = 16.dp), + onClick = {}, + containerColor = Gray10, + contentColor = Gray950, + disabledContentColor = Gray60, + ) { + Text( + text = "SOPT 회원 인증 완료", + style = SoptTheme.typography.body14M + ) + } + Spacer(modifier = Modifier.height(32.dp)) + } +} + +@Composable +private fun TopBar( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = ic_auth_process), + contentDescription = "상단 이미지" + ) + Spacer(modifier = Modifier.height(11.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(66.dp)) { + Text( + text = "SOPT 회원인증", + color = White, + style = SoptTheme.typography.label12SB + ) + Text( + text = "소셜 계정 연동", + color = Gray100, + style = SoptTheme.typography.label12SB + ) + } + Spacer(modifier = Modifier.height(60.dp)) + Text( + text = "SOPT 회원인증", + color = Gray10, + style = SoptTheme.typography.heading24B + ) + Text( + text = "Playground는 SOPT 회원만을 위한 공간이에요.\nSOPT 회원인증을 위해 전화번호를 입력해 주세요.", + color = Gray60, + style = SoptTheme.typography.label12SB + ) + } +} + +@Composable +private fun ErrorText( + error: ErrorCase +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painterResource(id = ic_auth_certification_error), + contentDescription = "에러 아이콘", + ) + Text( + text = error.message, + color = Red100, + style = SoptTheme.typography.label12SB + ) + } +} + +enum class ErrorCase(val message: String) { + CODE_ERROR("인증번호가 일치하지 않아요.\n번호를 확인한 후 다시 입력해 주세요."), + PHONE_ERROR("솝트 활동 시 사용한 전화번호가 아니예요.\n인증을 실패하신 경우 하단에서 다른 방법으로 인증할 수 있어요."), + TIME_ERROR("3분이 초과되었어요. 인증번호를 다시 요청해주세요.") +} + +@Preview(showBackground = true) +@Composable +private fun AuthCertificationPreview() { + SoptTheme { + CertificateMemberScreen() + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/CertificateAccountScreen.kt b/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/CertificateAccountScreen.kt new file mode 100644 index 000000000..bdc7da1a9 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/CertificateAccountScreen.kt @@ -0,0 +1,101 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.feature.socialaccount + +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.Black80 +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.Gray30 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.designsystem.White +import org.sopt.official.feature.auth.component.AuthButton +import org.sopt.official.feature.auth.component.AuthTextField +import org.sopt.official.feature.auth.component.PhoneCertification + +@Composable +fun CertificateAccountScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(48.dp)) + Text( + text = "가입 시 인증했던\n전화번호를 입력해주세요", + color = Gray10, + style = SoptTheme.typography.title24SB, + ) + Spacer(modifier = Modifier.height(36.dp)) + PhoneCertification( + onPhoneNumberClick = {}, + textColor = Gray30 + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = "인증번호를 입력해주세요.", + color = Gray30, + style = SoptTheme.typography.body14M + ) + Spacer(modifier = Modifier.height(12.dp)) + AuthTextField( + modifier = Modifier.fillMaxWidth(), + labelText = "", + hintText = "인증번호를 입력해 주세요.", + onTextChange = {} + ) + Spacer(modifier = Modifier.height(12.dp)) + AuthButton( + modifier = Modifier.fillMaxWidth(), + padding = PaddingValues(vertical = 16.dp), + onClick = {}, + containerColor = White, + contentColor = Black80, + ) { + Text( + text = "인증 완료하기", + style = SoptTheme.typography.body16M + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SocialAccountPreview() { + SoptTheme { + CertificateAccountScreen() + } +} diff --git a/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/ChangeAccountScreen.kt b/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/ChangeAccountScreen.kt new file mode 100644 index 000000000..76edc4276 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/auth/feature/socialaccount/ChangeAccountScreen.kt @@ -0,0 +1,88 @@ +/* + * MIT License + * Copyright 2024 SOPT - Shout Our Passion Together + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.sopt.official.feature.auth.feature.socialaccount + +import androidx.compose.foundation.Image +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.Gray10 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.auth.component.AuthButton + +@Composable +fun ChangeAccountScreen( + onGoogleLoginCLick: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.height(48.dp)) + Text( + text = "새로 연결할\n소셜 계정을 선택해주세요", + color = Gray10, + style = SoptTheme.typography.title24SB, + ) + Spacer(modifier = Modifier.height(36.dp)) + AuthButton( + padding = PaddingValues(vertical = 13.dp), + onClick = onGoogleLoginCLick, + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.ic_auth_google), + contentDescription = "구글 로고", + modifier = Modifier.padding(end = 8.dp) + ) + Text( + text = "Google 계정 연결하기", + style = SoptTheme.typography.label16SB + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ChangeAccountPreview() { + SoptTheme { + ChangeAccountScreen( + onGoogleLoginCLick = {} + ) + } +} diff --git a/app/src/main/res/drawable/ic_auth_alert_circle.xml b/app/src/main/res/drawable/ic_auth_alert_circle.xml new file mode 100644 index 000000000..02b736aee --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_alert_circle.xml @@ -0,0 +1,37 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_auth_arrow_right.xml b/app/src/main/res/drawable/ic_auth_arrow_right.xml new file mode 100644 index 000000000..48f5a830f --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_arrow_right.xml @@ -0,0 +1,37 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_auth_certification_error.xml b/app/src/main/res/drawable/ic_auth_certification_error.xml new file mode 100644 index 000000000..2c2d3ac5e --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_certification_error.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_auth_certification_notice.xml b/app/src/main/res/drawable/ic_auth_certification_notice.xml new file mode 100644 index 000000000..4aea15846 --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_certification_notice.xml @@ -0,0 +1,40 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_auth_error.xml b/app/src/main/res/drawable/ic_auth_error.xml new file mode 100644 index 000000000..747c8ea8e --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_error.xml @@ -0,0 +1,36 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_auth_google.xml b/app/src/main/res/drawable/ic_auth_google.xml new file mode 100644 index 000000000..a9380fd2a --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_google.xml @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_auth_memeber_error.xml b/app/src/main/res/drawable/ic_auth_memeber_error.xml new file mode 100644 index 000000000..48b20a791 --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_memeber_error.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_auth_process.xml b/app/src/main/res/drawable/ic_auth_process.xml new file mode 100644 index 000000000..153bc5103 --- /dev/null +++ b/app/src/main/res/drawable/ic_auth_process.xml @@ -0,0 +1,45 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/activity_auth.xml b/app/src/main/res/layout/activity_auth.xml deleted file mode 100644 index dc79cf18a..000000000 --- a/app/src/main/res/layout/activity_auth.xml +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index 886d724d5..7bf1805ed 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -2,7 +2,7 @@