This commit is contained in:
erike 2023-11-16 01:25:38 -06:00
parent 417e9ea322
commit 1da51e223e
11 changed files with 239 additions and 6 deletions

View File

@ -1,5 +1,6 @@
package com.isolaatti.auth.data
import com.isolaatti.BuildConfig
import com.isolaatti.settings.data.KeyValueDao
import com.isolaatti.settings.data.KeyValueEntity
import com.isolaatti.auth.data.remote.AuthTokenDto
@ -26,7 +27,7 @@ class AuthRepositoryImpl @Inject constructor(
): Flow<Resource<Boolean>> = flow {
try {
val res =
authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse()
authApi.signInWithEmailAndPassword(BuildConfig.clientId, BuildConfig.secret, Credential(email, password)).awaitResponse()
if(res.isSuccessful) {
val dto = res.body()

View File

@ -11,7 +11,7 @@ interface AuthApi {
fun validateTokenUrl(@Header("sessionToken") sessionToken: String): Call<AuthTokenVerificationDto>
@POST("LogIn")
fun signInWithEmailAndPassword(@Body credential: Credential): Call<AuthTokenDto>
fun signInWithEmailAndPassword(@Header("apiClientId") clientId: String, @Header("apiClientSecret") clientSecret: String, @Body credential: Credential): Call<AuthTokenDto>
@GET("LogIn/SignOut")
fun signOut(): Call<Nothing>

View File

@ -2,12 +2,15 @@ package com.isolaatti.sign_up.data
import com.isolaatti.sign_up.data.dto.CodeValidationDto
import com.isolaatti.sign_up.data.dto.DataDto
import com.isolaatti.sign_up.data.dto.NameAvailabilityDto
import com.isolaatti.sign_up.data.dto.ResultDto
import com.isolaatti.sign_up.data.dto.SignUpWithCodeDto
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
interface SignUpApi {
@ -31,4 +34,9 @@ interface SignUpApi {
@Header("clientSecret") apiSecret: String,
@Body dto: SignUpWithCodeDto
): Call<ResultDto>
@GET("usernames/check")
fun checkNameAvailability(
@Query("username") username: String
): Call<NameAvailabilityDto>
}

View File

@ -57,7 +57,23 @@ class SignUpRepositoryImpl @Inject constructor(private val signUpApi: SignUpApi)
SignUpWithCodeDto(username, password, displayName, code)
).awaitResponse()
if(response.isSuccessful){
response.body()?.let { GetCodeResult.valueOf(it.result)}
response.body()?.let { emit(Resource.Success(SignUpResult.valueOf(it.result)))}
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}
} catch (e: IllegalArgumentException) {
emit(Resource.Error(Resource.Error.ErrorType.OtherError, "Could not map response. $e"))
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun checkUsernameAvailability(username: String): Flow<Resource<Boolean>> = flow {
emit(Resource.Loading())
try {
val response = signUpApi.checkNameAvailability(username).awaitResponse()
if(response.isSuccessful){
response.body()?.let { emit(Resource.Success(it.available))}
} else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
}

View File

@ -0,0 +1,5 @@
package com.isolaatti.sign_up.data.dto
data class NameAvailabilityDto(
val available: Boolean
)

View File

@ -9,4 +9,5 @@ interface SignUpRepository {
fun getCode(email: String): Flow<Resource<GetCodeResult>>
fun validateCode(code: String): Flow<Resource<Boolean>>
fun signUpWithCode(username: String, displayName: String, password: String, code: String): Flow<Resource<SignUpResult>>
fun checkUsernameAvailability(username: String): Flow<Resource<Boolean>>
}

View File

@ -0,0 +1,81 @@
package com.isolaatti.sign_up.presentation
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.isolaatti.sign_up.data.SignUpApi
import com.isolaatti.sign_up.domain.SignUpRepository
import com.isolaatti.sign_up.domain.entity.SignUpResult
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class MakeAccountViewModel @Inject constructor(private val signUpRepository: SignUpRepository) : ViewModel() {
val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val passwordIsValid: MutableLiveData<Boolean> = MutableLiveData()
var usernameIsValid: MutableLiveData<Boolean> = MutableLiveData()
val displayNameIsValid: MutableLiveData<Boolean> = MutableLiveData()
val signUpResult: MutableLiveData<Resource<SignUpResult>?> = MutableLiveData()
var code: String? = null
private fun validateForm() {
formIsValid.value = passwordIsValid.value == true && usernameIsValid.value == true
}
var password: String = ""
set(value) {
field = value
passwordIsValid.value = value.length > 7
validateForm()
}
var username: String = ""
set(value) {
field = value
checkUsernameAvailability(value)
}
var displayName: String = ""
set(value) {
field = value
displayNameIsValid.value = value.isNotBlank()
}
private fun checkUsernameAvailability(username: String) {
if(username.length < 3) {
usernameIsValid.value = false
validateForm()
return
}
viewModelScope.launch {
signUpRepository.checkUsernameAvailability(username).onEach {
when(it) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
usernameIsValid.postValue(it.data!!)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun makeAccount() {
if(code==null) {
return
}
viewModelScope.launch {
signUpRepository.signUpWithCode(username, displayName, password, code!!).onEach {
signUpResult.postValue(it)
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
}

View File

@ -1,15 +1,35 @@
package com.isolaatti.sign_up.ui
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.databinding.FragmentMakeAccountBinding
import com.isolaatti.sign_up.presentation.MakeAccountViewModel
import com.isolaatti.sign_up.presentation.SignUpViewModel
import com.isolaatti.utils.Resource
import com.isolaatti.utils.textChanges
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@AndroidEntryPoint
class MakeAccountFragment : Fragment() {
private lateinit var binding: FragmentMakeAccountBinding
private val activityViewModel: SignUpViewModel by activityViewModels()
private val viewModel: MakeAccountViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -19,4 +39,81 @@ class MakeAccountFragment : Fragment() {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.code = activityViewModel.code
setupListeners()
setupObservers()
}
@OptIn(FlowPreview::class)
private fun setupListeners() {
binding.readTermsAndConditions.setOnClickListener {
val termsAndConditionsCustomTabsIntent = CustomTabsIntent.Builder().build()
termsAndConditionsCustomTabsIntent.launchUrl(requireContext(), Uri.parse(BuildConfig.terms))
}
binding.privacyPolicy.setOnClickListener {
val termsAndConditionsCustomTabsIntent = CustomTabsIntent.Builder().build()
termsAndConditionsCustomTabsIntent.launchUrl(requireContext(), Uri.parse(BuildConfig.privacyPolicy))
}
binding.password.editText?.doOnTextChanged { text, start, before, count ->
viewModel.password = text.toString()
}
binding.textUsername.editText?.textChanges()?.debounce(300)?.onEach { text ->
viewModel.username = text.toString()
}?.launchIn(lifecycleScope)
binding.textDisplayName.editText?.doOnTextChanged { text, _, _, _ ->
viewModel.displayName = text.toString()
}
binding.signUpButton.setOnClickListener {
viewModel.makeAccount()
}
}
private fun setupObservers() {
viewModel.formIsValid.observe(viewLifecycleOwner) {
binding.signUpButton.isEnabled = it
}
viewModel.usernameIsValid.observe(viewLifecycleOwner) {
binding.textUsername.error = if(it) {
null
} else {
getText(R.string.username_invalid_feedback)
}
}
viewModel.passwordIsValid.observe(viewLifecycleOwner) {
binding.password.error = if(it) {
null
} else {
getText(R.string.password_invalid_feedback)
}
}
viewModel.displayNameIsValid.observe(viewLifecycleOwner) {
binding.textDisplayName.error = if(it){
null
} else {
getString(R.string.display_name_invalid_feedback)
}
}
viewModel.signUpResult.observe(viewLifecycleOwner) {
when(it) {
is Resource.Error -> {}
is Resource.Loading -> {}
is Resource.Success -> {
}
null -> {}
}
viewModel.signUpResult.value = null
}
}
}

View File

@ -0,0 +1,18 @@
package com.isolaatti.utils
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import com.google.android.gms.common.internal.Preconditions.checkMainThread
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.onStart
fun EditText.textChanges(): Flow<CharSequence?> {
return callbackFlow {
checkMainThread()
val listener = doOnTextChanged { text, _, _, _ -> trySend(text) }
awaitClose { removeTextChangedListener(listener) }
}.onStart { emit(text) }
}

View File

@ -73,10 +73,12 @@
style="?attr/textInputOutlinedStyle"
android:layout_marginTop="4dp"
android:layout_marginHorizontal="16dp"
app:endIconMode="password_toggle"
android:hint="@string/password">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPassword"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
@ -106,11 +108,12 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/signUpButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp"
android:text="Go"/>
android:text="@string/sign_up"/>
</LinearLayout>

View File

@ -105,9 +105,12 @@
<string name="loading">Loading...</string>
<string name="unique_username">Unique username</string>
<string name="display_name">Display name</string>
<string name="create_a_password">Create a fairly password</string>
<string name="create_a_password">Create a fairly strong password</string>
<string name="let_s_make_your_account">Let\'s make your account</string>
<string name="before_making_your_account_please_read_these_legal_terms">Before making your account, please read and accept these legal terms.</string>
<string name="before_making_your_account_please_read_these_legal_terms">Before making your account, please read and accept these legal terms. By continuing you are stating that you have read and accepted both.</string>
<string name="terms_and_conditions">Terms and conditions</string>
<string name="privacy_policy">Privacy policy</string>
<string name="username_invalid_feedback">Username is not available or is invalid</string>
<string name="password_invalid_feedback">Password must be at least 8 characters long</string>
<string name="display_name_invalid_feedback">Please provide a name. This does not have to be unique and can be your real name or not.</string>
</resources>