-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #73 from boostcampwm2023/feat/and/signup
νμκ°μ μ λ ₯ ꡬν
- Loading branch information
Showing
9 changed files
with
559 additions
and
126 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
177 changes: 175 additions & 2 deletions
177
android/app/src/main/java/app/priceguard/ui/signup/SignupActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
190
android/app/src/main/java/app/priceguard/ui/signup/SignupViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.