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

View File

@ -11,7 +11,7 @@ interface AuthApi {
fun validateTokenUrl(@Header("sessionToken") sessionToken: String): Call<AuthTokenVerificationDto> fun validateTokenUrl(@Header("sessionToken") sessionToken: String): Call<AuthTokenVerificationDto>
@POST("LogIn") @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") @GET("LogIn/SignOut")
fun signOut(): Call<Nothing> 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.CodeValidationDto
import com.isolaatti.sign_up.data.dto.DataDto 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.ResultDto
import com.isolaatti.sign_up.data.dto.SignUpWithCodeDto import com.isolaatti.sign_up.data.dto.SignUpWithCodeDto
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header import retrofit2.http.Header
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Query
interface SignUpApi { interface SignUpApi {
@ -31,4 +34,9 @@ interface SignUpApi {
@Header("clientSecret") apiSecret: String, @Header("clientSecret") apiSecret: String,
@Body dto: SignUpWithCodeDto @Body dto: SignUpWithCodeDto
): Call<ResultDto> ): 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) SignUpWithCodeDto(username, password, displayName, code)
).awaitResponse() ).awaitResponse()
if(response.isSuccessful){ 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 { } else {
emit(Resource.Error(Resource.Error.mapErrorCode(response.code()))) 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 getCode(email: String): Flow<Resource<GetCodeResult>>
fun validateCode(code: String): Flow<Resource<Boolean>> fun validateCode(code: String): Flow<Resource<Boolean>>
fun signUpWithCode(username: String, displayName: String, password: String, code: String): Flow<Resource<SignUpResult>> 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 package com.isolaatti.sign_up.ui
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment 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.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() { class MakeAccountFragment : Fragment() {
private lateinit var binding: FragmentMakeAccountBinding private lateinit var binding: FragmentMakeAccountBinding
private val activityViewModel: SignUpViewModel by activityViewModels()
private val viewModel: MakeAccountViewModel by viewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -19,4 +39,81 @@ class MakeAccountFragment : Fragment() {
return binding.root 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" style="?attr/textInputOutlinedStyle"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
app:endIconMode="password_toggle"
android:hint="@string/password"> android:hint="@string/password">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:inputType="textPassword"
android:maxLines="1" /> android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
@ -106,11 +108,12 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/signUpButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:text="Go"/> android:text="@string/sign_up"/>
</LinearLayout> </LinearLayout>

View File

@ -105,9 +105,12 @@
<string name="loading">Loading...</string> <string name="loading">Loading...</string>
<string name="unique_username">Unique username</string> <string name="unique_username">Unique username</string>
<string name="display_name">Display name</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="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="terms_and_conditions">Terms and conditions</string>
<string name="privacy_policy">Privacy policy</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> </resources>