From 1da51e223e8b5c5d6e267c356ee986e191fe73d9 Mon Sep 17 00:00:00 2001 From: erike Date: Thu, 16 Nov 2023 01:25:38 -0600 Subject: [PATCH] WIP --- .../isolaatti/auth/data/AuthRepositoryImpl.kt | 3 +- .../com/isolaatti/auth/data/remote/AuthApi.kt | 2 +- .../com/isolaatti/sign_up/data/SignUpApi.kt | 8 ++ .../sign_up/data/SignUpRepositoryImpl.kt | 18 +++- .../sign_up/data/dto/NameAvailabilityDto.kt | 5 + .../sign_up/domain/SignUpRepository.kt | 1 + .../presentation/MakeAccountViewModel.kt | 81 ++++++++++++++++ .../sign_up/ui/MakeAccountFragment.kt | 97 +++++++++++++++++++ .../utils/EditTextTextChangesFlow.kt | 18 ++++ .../main/res/layout/fragment_make_account.xml | 5 +- app/src/main/res/values/strings.xml | 7 +- 11 files changed, 239 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/isolaatti/sign_up/data/dto/NameAvailabilityDto.kt create mode 100644 app/src/main/java/com/isolaatti/sign_up/presentation/MakeAccountViewModel.kt create mode 100644 app/src/main/java/com/isolaatti/utils/EditTextTextChangesFlow.kt diff --git a/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt index ecfccc3..a1d8754 100644 --- a/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/auth/data/AuthRepositoryImpl.kt @@ -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> = 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() diff --git a/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt b/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt index f8bef8f..20fe0ee 100644 --- a/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt +++ b/app/src/main/java/com/isolaatti/auth/data/remote/AuthApi.kt @@ -11,7 +11,7 @@ interface AuthApi { fun validateTokenUrl(@Header("sessionToken") sessionToken: String): Call @POST("LogIn") - fun signInWithEmailAndPassword(@Body credential: Credential): Call + fun signInWithEmailAndPassword(@Header("apiClientId") clientId: String, @Header("apiClientSecret") clientSecret: String, @Body credential: Credential): Call @GET("LogIn/SignOut") fun signOut(): Call diff --git a/app/src/main/java/com/isolaatti/sign_up/data/SignUpApi.kt b/app/src/main/java/com/isolaatti/sign_up/data/SignUpApi.kt index 83847b9..87cc538 100644 --- a/app/src/main/java/com/isolaatti/sign_up/data/SignUpApi.kt +++ b/app/src/main/java/com/isolaatti/sign_up/data/SignUpApi.kt @@ -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 + + @GET("usernames/check") + fun checkNameAvailability( + @Query("username") username: String + ): Call } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/sign_up/data/SignUpRepositoryImpl.kt b/app/src/main/java/com/isolaatti/sign_up/data/SignUpRepositoryImpl.kt index 35f6a56..097dac6 100644 --- a/app/src/main/java/com/isolaatti/sign_up/data/SignUpRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/sign_up/data/SignUpRepositoryImpl.kt @@ -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> = 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()))) } diff --git a/app/src/main/java/com/isolaatti/sign_up/data/dto/NameAvailabilityDto.kt b/app/src/main/java/com/isolaatti/sign_up/data/dto/NameAvailabilityDto.kt new file mode 100644 index 0000000..5abd281 --- /dev/null +++ b/app/src/main/java/com/isolaatti/sign_up/data/dto/NameAvailabilityDto.kt @@ -0,0 +1,5 @@ +package com.isolaatti.sign_up.data.dto + +data class NameAvailabilityDto( + val available: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/sign_up/domain/SignUpRepository.kt b/app/src/main/java/com/isolaatti/sign_up/domain/SignUpRepository.kt index ce30cee..4ee335b 100644 --- a/app/src/main/java/com/isolaatti/sign_up/domain/SignUpRepository.kt +++ b/app/src/main/java/com/isolaatti/sign_up/domain/SignUpRepository.kt @@ -9,4 +9,5 @@ interface SignUpRepository { fun getCode(email: String): Flow> fun validateCode(code: String): Flow> fun signUpWithCode(username: String, displayName: String, password: String, code: String): Flow> + fun checkUsernameAvailability(username: String): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/sign_up/presentation/MakeAccountViewModel.kt b/app/src/main/java/com/isolaatti/sign_up/presentation/MakeAccountViewModel.kt new file mode 100644 index 0000000..76f86b6 --- /dev/null +++ b/app/src/main/java/com/isolaatti/sign_up/presentation/MakeAccountViewModel.kt @@ -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 = MutableLiveData(false) + val passwordIsValid: MutableLiveData = MutableLiveData() + var usernameIsValid: MutableLiveData = MutableLiveData() + val displayNameIsValid: MutableLiveData = MutableLiveData() + val signUpResult: MutableLiveData?> = 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt b/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt index a8dca09..9a1f6d3 100644 --- a/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt +++ b/app/src/main/java/com/isolaatti/sign_up/ui/MakeAccountFragment.kt @@ -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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/utils/EditTextTextChangesFlow.kt b/app/src/main/java/com/isolaatti/utils/EditTextTextChangesFlow.kt new file mode 100644 index 0000000..2d24fb4 --- /dev/null +++ b/app/src/main/java/com/isolaatti/utils/EditTextTextChangesFlow.kt @@ -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 { + return callbackFlow { + checkMainThread() + + val listener = doOnTextChanged { text, _, _, _ -> trySend(text) } + awaitClose { removeTextChangedListener(listener) } + }.onStart { emit(text) } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_make_account.xml b/app/src/main/res/layout/fragment_make_account.xml index 1626319..4480a41 100644 --- a/app/src/main/res/layout/fragment_make_account.xml +++ b/app/src/main/res/layout/fragment_make_account.xml @@ -73,10 +73,12 @@ style="?attr/textInputOutlinedStyle" android:layout_marginTop="4dp" android:layout_marginHorizontal="16dp" + app:endIconMode="password_toggle" android:hint="@string/password"> @@ -106,11 +108,12 @@ + android:text="@string/sign_up"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7aab891..c604bdf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,9 +105,12 @@ Loading... Unique username Display name - Create a fairly password + Create a fairly strong password Let\'s make your account - Before making your account, please read and accept 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. Terms and conditions Privacy policy + Username is not available or is invalid + Password must be at least 8 characters long + Please provide a name. This does not have to be unique and can be your real name or not. \ No newline at end of file