Skip to content

Commit

Permalink
Merge pull request #73 from boostcampwm2023/feat/and/signup
Browse files Browse the repository at this point in the history
νšŒμ›κ°€μž… μž…λ ₯ κ΅¬ν˜„
  • Loading branch information
EunhoKang authored Nov 16, 2023
2 parents c784176 + d5bba09 commit 811e85d
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 126 deletions.
2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
android:roundIcon="@mipmap/ic_priceguard_round"
android:supportsRtl="true"
android:theme="@style/Theme.PriceGuard"
tools:targetApi="31">
tools:targetApi="34">
<activity
android:name=".ui.intro.IntroActivity"
android:exported="false" />
Expand Down
177 changes: 175 additions & 2 deletions android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,184 @@
package app.priceguard.ui.signup

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.NestedScrollView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import app.priceguard.R
import app.priceguard.databinding.ActivitySignupBinding
import app.priceguard.ui.signup.SignupViewModel.SignupEvent
import app.priceguard.ui.signup.SignupViewModel.SignupUIState
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.button.MaterialButton
import com.google.android.material.progressindicator.CircularProgressIndicatorSpec
import com.google.android.material.progressindicator.IndeterminateDrawable
import kotlinx.coroutines.launch

class SignupActivity : AppCompatActivity() {

private lateinit var binding: ActivitySignupBinding
private val signupViewModel: SignupViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_signup)
binding.vm = signupViewModel
binding.lifecycleOwner = this

setNavigationButton()
disableAppBarScroll()
observeState()
}

private fun disableAppBarScroll() {
val clLayoutParams = binding.ablSignupTopbar.layoutParams as CoordinatorLayout.LayoutParams
val scrollView: NestedScrollView = binding.nsvSignupContent
val viewTreeObserver = scrollView.viewTreeObserver
val disabledAblBehavior = getAblBehavior(false)
val enabledAblBehavior = getAblBehavior(true)

viewTreeObserver.addOnGlobalLayoutListener {
if (scrollView.measuredHeight - scrollView.getChildAt(0).height >= 0) {
clLayoutParams.behavior = disabledAblBehavior
} else {
clLayoutParams.behavior = enabledAblBehavior
}
}
}

private fun getAblBehavior(canDrag: Boolean): AppBarLayout.Behavior {
val ablBehavior = AppBarLayout.Behavior()
ablBehavior.setDragCallback(object : DragCallback() {
override fun canDrag(appBarLayout: AppBarLayout): Boolean {
return canDrag
}
})
return ablBehavior
}

private fun handleSignupEvent(event: SignupEvent) {
val spec =
CircularProgressIndicatorSpec(
this,
null,
0,
R.style.Theme_PriceGuard_CircularProgressIndicator
)

val progressIndicatorDrawable =
IndeterminateDrawable.createCircularDrawable(this, spec).apply {
setVisible(true, true)
}

when (event) {
is SignupEvent.SignupStart -> {
(binding.btnSignupSignup as MaterialButton).icon = progressIndicatorDrawable
}

is SignupEvent.SignupFinish -> {
(binding.btnSignupSignup as MaterialButton).icon = null
}

is SignupEvent.SignupError -> {}
}
}

private fun setNavigationButton() {
binding.mtSignupTopbar.setNavigationOnClickListener {
finish()
}
}

private fun observeState() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
signupViewModel.state.collect { state ->
updateNameTextFieldUI(state)
updateEmailTextFieldUI(state)
updatePasswordTextFieldUI(state)
updateRetypePasswordTextFieldUI(state)
}
}
}

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
signupViewModel.eventFlow.collect { event ->
handleSignupEvent(event)
}
}
}
}

private fun updateNameTextFieldUI(state: SignupUIState) {
when (state.isNameError) {
true -> {
binding.tilSignupName.error = getString(R.string.name_required)
}

else -> {
binding.tilSignupName.error = null
}
}
}

private fun updateEmailTextFieldUI(state: SignupUIState) {
when (state.isEmailError) {
null -> {
binding.tilSignupEmail.error = null
binding.tilSignupEmail.helperText = " "
}

true -> {
binding.tilSignupEmail.error = getString(R.string.invalid_email)
}

false -> {
binding.tilSignupEmail.error = null
binding.tilSignupEmail.helperText = getString(R.string.valid_email)
}
}
}

private fun updatePasswordTextFieldUI(state: SignupUIState) {
when (state.isPasswordError) {
null -> {
binding.tilSignupPassword.error = null
binding.tilSignupPassword.helperText = " "
}

true -> {
binding.tilSignupPassword.error = getString(R.string.invalid_password)
}

false -> {
binding.tilSignupPassword.error = null
binding.tilSignupPassword.helperText = getString(R.string.valid_password)
}
}
}

private fun updateRetypePasswordTextFieldUI(state: SignupUIState) {
when (state.isRetypePasswordError) {
null -> {
binding.tilSignupRetypePassword.error = null
binding.tilSignupRetypePassword.helperText = " "
}

true -> {
binding.tilSignupRetypePassword.error = getString(R.string.password_mismatch)
}

false -> {
binding.tilSignupRetypePassword.error = null
binding.tilSignupRetypePassword.helperText = getString(R.string.password_match)
}
}
}
}
}
190 changes: 190 additions & 0 deletions android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package app.priceguard.ui.signup

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class SignupViewModel : ViewModel() {

data class SignupUIState(
val name: String = "",
val email: String = "",
val password: String = "",
val retypePassword: String = "",
val isSignupReady: Boolean = false,
val isNameError: Boolean? = null,
val isEmailError: Boolean? = null,
val isPasswordError: Boolean? = null,
val isRetypePasswordError: Boolean? = null,
val isSignupStarted: Boolean = false
)

private val emailPattern =
"""^[\w.+-]+@((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$""".toRegex()
private val passwordPattern =
"""^(?=[A-Za-z\d!@#$%^&*]*\d)(?=[A-Za-z\d!@#$%^&*]*[a-z])(?=[A-Za-z\d!@#$%^&*]*[A-Z])(?=[A-Za-z\d!@#$%^&*]*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,16}$""".toRegex()

private val _state: MutableStateFlow<SignupUIState> = MutableStateFlow(SignupUIState())
val state: StateFlow<SignupUIState> = _state.asStateFlow()

private val _eventFlow: MutableSharedFlow<SignupEvent> = MutableSharedFlow(replay = 0)
val eventFlow: SharedFlow<SignupEvent> = _eventFlow.asSharedFlow()

fun signup() {
viewModelScope.launch {
if (_state.value.isSignupStarted) {
Log.d("Signup", "Signup already requested. Skipping")
return@launch
}

sendEvent(SignupEvent.SignupStart)
updateSignupStarted(true)
Log.d("ViewModel", "Event Start Sent")
delay(3000L)
// TODO: μ œκ±°ν•˜κ³  Signup λ„€νŠΈμ›Œν¬ 둜직 λ„£κΈ°
// TODO: νšŒμ›κ°€μž… μ„±κ³΅μ‹œ 둜그인 정보 μ €μž₯ν•˜κΈ°
// TODO: νšŒμ›κ°€μž… μ‹€νŒ¨μ‹œ 메세지 ν‘œμ‹œν•˜κΈ°
sendEvent(SignupEvent.SignupFinish)
updateSignupStarted(false)
Log.d("ViewModel", "Event Finish Sent")
}
}

fun updateName(name: String) {
_state.value = _state.value.copy(name = name)

updateNameError()
updateIsSignupReady()
}

fun updateEmail(email: String) {
_state.value = _state.value.copy(email = email)

updateEmailError()
updateIsSignupReady()
}

fun updatePassword(password: String) {
_state.value = _state.value.copy(password = password)

updatePasswordError()
updateRetypePasswordError()
updateIsSignupReady()
}

fun updateRetypePassword(retypePassword: String) {
_state.value = _state.value.copy(retypePassword = retypePassword)

updateRetypePasswordError()
updateIsSignupReady()
}

private fun isValidName(): Boolean {
return _state.value.name.isNotBlank()
}

private fun isValidEmail(): Boolean {
return emailPattern.matchEntire(_state.value.email) != null
}

private fun isValidPassword(): Boolean {
return passwordPattern.matchEntire(_state.value.password) != null
}

private fun isValidRetypePassword(): Boolean {
return _state.value.retypePassword.isNotBlank() && _state.value.password == _state.value.retypePassword
}

private fun sendEvent(event: SignupEvent) {
viewModelScope.launch {
_eventFlow.emit(event)
}
}

private fun updateIsSignupReady() {
_state.value =
_state.value.copy(isSignupReady = isValidName() && isValidEmail() && isValidPassword() && isValidRetypePassword())
}

private fun updateNameError() {
_state.value.let { uiState ->
if (isValidName()) {
_state.value = uiState.copy(isNameError = false)
} else {
_state.value = uiState.copy(isNameError = true)
}
}
}

private fun updateEmailError() {
_state.value.let { uiState ->
when {
isValidEmail() -> {
_state.value = uiState.copy(isEmailError = false)
}

uiState.email.isEmpty() -> {
_state.value = uiState.copy(isEmailError = null)
}

else -> {
_state.value = uiState.copy(isEmailError = true)
}
}
}
}

private fun updatePasswordError() {
_state.value.let { uiState ->
when {
isValidPassword() -> {
_state.value = uiState.copy(isPasswordError = false)
}

uiState.password.isEmpty() -> {
_state.value = uiState.copy(isPasswordError = null)
}

else -> {
_state.value = uiState.copy(isPasswordError = true)
}
}
}
}

private fun updateRetypePasswordError() {
_state.value.let { uiState ->
when {
isValidRetypePassword() -> {
_state.value = uiState.copy(isRetypePasswordError = false)
}

uiState.retypePassword.isEmpty() -> {
_state.value = uiState.copy(isRetypePasswordError = null)
}

else -> {
_state.value = uiState.copy(isRetypePasswordError = true)
}
}
}
}

private fun updateSignupStarted(started: Boolean) {
_state.value = _state.value.copy(isSignupStarted = started)
}

sealed class SignupEvent {
object SignupStart : SignupEvent()
object SignupFinish : SignupEvent()
object SignupError : SignupEvent()
}
}
2 changes: 1 addition & 1 deletion android/app/src/main/res/drawable/ic_back.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:tint="?attr/colorOnSurfaceVariant" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>
Loading

0 comments on commit 811e85d

Please sign in to comment.