WIP
This commit is contained in:
parent
417e9ea322
commit
1da51e223e
@ -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()
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
}
|
}
|
||||||
@ -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())))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.isolaatti.sign_up.data.dto
|
||||||
|
|
||||||
|
data class NameAvailabilityDto(
|
||||||
|
val available: Boolean
|
||||||
|
)
|
||||||
@ -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>>
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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) }
|
||||||
|
}
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
Loading…
x
Reference in New Issue
Block a user