diff --git a/app/build.gradle b/app/build.gradle index 5cbed8e..7faf737 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,7 +16,7 @@ android { versionName "1.0" buildConfigField "Boolean", "USE_MOCK_BACKEND_API", 'false' - buildConfigField "String", "BACKEND_API_URL", '""' + buildConfigField "String", "BACKEND_API_URL", '"https://android-course-backend.herokuapp.com/"' testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -54,21 +54,17 @@ android { dimension "deployment-environment" applicationIdSuffix ".dev" versionNameSuffix ".dev" - buildConfigField "String", "BACKEND_API_URL", '""' } stage { dimension "deployment-environment" applicationIdSuffix ".stage" versionNameSuffix ".stage" - buildConfigField "String", "BACKEND_API_URL", '""' } prod { dimension "deployment-environment" versionNameSuffix ".prod" // Despite the default config, all the variables are specified explicitly here, // because this product flavor is the most sensitive to errors. - buildConfigField "Boolean", "USE_MOCK_BACKEND_API", 'false' - buildConfigField "String", "BACKEND_API_URL", '""' } } diff --git a/app/src/main/java/com/example/mobileapp/data/network/Api.kt b/app/src/main/java/com/example/mobileapp/data/network/Api.kt index 9bd8f3e..35b280f 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/Api.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/Api.kt @@ -3,21 +3,18 @@ package com.example.mobileapp import com.example.mobileapp.data.network.request.CreateProfileRequest import com.example.mobileapp.data.network.request.RefreshAuthTokensRequest import com.example.mobileapp.data.network.request.SignInWithEmailRequest -import com.example.mobileapp.data.network.response.VerificationTokenResponse import com.example.mobileapp.data.network.response.error.* import com.example.mobileapp.domain.AuthTokens import com.example.mobileapp.domain.Post import com.example.mobileapp.domain.User import com.haroldadmin.cnradapter.NetworkResponse -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass import retrofit2.http.* interface Api { @GET("users") suspend fun getUsers(): NetworkResponse, Unit> - @POST("auth/sign-in-email") + @POST("auth/sign-in-with-email") suspend fun signInWithEmail( @Body request: SignInWithEmailRequest ): NetworkResponse @@ -37,13 +34,16 @@ interface Api { @Query("code") code: String, @Query("email") email: String?, @Query("phone_number") phoneNumber: String? - ): NetworkResponse + ): NetworkResponse - @PUT("registration/create-profile") + @POST("registration/create-profile") suspend fun createProfile( @Body request: CreateProfileRequest - ): NetworkResponse + ): NetworkResponse @POST("posts") suspend fun getPost(): NetworkResponse, Unit> + + @GET("users/get-profile") + suspend fun getProfile(): NetworkResponse } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/data/network/MockApi.kt b/app/src/main/java/com/example/mobileapp/data/network/MockApi.kt index a5cc4f4..de4fde4 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/MockApi.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/MockApi.kt @@ -4,7 +4,6 @@ import com.example.mobileapp.Api import com.example.mobileapp.data.network.request.CreateProfileRequest import com.example.mobileapp.data.network.request.RefreshAuthTokensRequest import com.example.mobileapp.data.network.request.SignInWithEmailRequest -import com.example.mobileapp.data.network.response.VerificationTokenResponse import com.example.mobileapp.data.network.response.error.* import com.example.mobileapp.domain.AuthTokens import com.example.mobileapp.domain.Post @@ -16,6 +15,58 @@ import kotlin.random.Random class MockApi : Api { + private var idCur = 12L + private val userList = mutableListOf( + User( + id = 7, + firstName = "Michael", + lastName = "Lawson", + avatarUrl = "https://reqres.in/img/faces/7-image.jpg", + username = "Michael", + groupName = "Б09.мкн" + ), + User( + id = 4, + firstName = "George", + lastName = "VI", + username = "Gosha", + avatarUrl = null, + groupName = null + ), + User( + id = 8, + firstName = "Masha", + lastName = "Nazarova", + username = "Mary", + avatarUrl = null, + groupName = null + ), + User( + id = 9, + firstName = "Igor", + lastName = "Petrov", + username = "igor95", + avatarUrl = null, + groupName = null + ), + User( + id = 10, + firstName = "Masha", + lastName = "Nazarova", + username = "Mary2", + avatarUrl = null, + groupName = null + ), + User( + id = 11, + firstName = "Masha", + lastName = "Nazarova", + username = "Mary3", + avatarUrl = null, + groupName = null + ) + ) + private val randomizer = Random(1337) override suspend fun getUsers(): NetworkResponse, Unit> { @@ -23,16 +74,7 @@ class MockApi : Api { Timber.d("Try to load users. Success: %s", success) if (success) return NetworkResponse.Success( - body = listOf( - User( - id = 7, - firstName = "Michael", - lastName = "Lawson", - avatarUrl = "https://reqres.in/img/faces/7-image.jpg", - username = "Michael", - groupName = "Б09.мкн" - ) - ), + body = userList, code = 200 ) if (randomizer.nextBoolean()) @@ -58,22 +100,39 @@ class MockApi : Api { } override suspend fun sendRegistrationVerificationCode(email: String): NetworkResponse { - TODO("Not yet implemented") + return NetworkResponse.Success(body = Unit, code = 200) } override suspend fun verifyRegistrationCode( code: String, email: String?, phoneNumber: String? - ): NetworkResponse { + ): NetworkResponse { TODO("Not yet implemented") } - override suspend fun createProfile(request: CreateProfileRequest): NetworkResponse { - TODO("Not yet implemented") + override suspend fun createProfile(request: CreateProfileRequest): NetworkResponse { + idCur += 1 + val newUser = User( + id = idCur, + username = request.username, + firstName = request.firstName, + lastName = request.lastName, + groupName = null, + avatarUrl = null + ) + userList.add(newUser) + return NetworkResponse.Success(body = newUser, code = 204) } override suspend fun getPost(): NetworkResponse, Unit> { TODO("Not yet implemented") } + + override suspend fun getProfile(): NetworkResponse { + return NetworkResponse.Success( + userList[0], + code = 200 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/data/network/request/CreateProfileRequest.kt b/app/src/main/java/com/example/mobileapp/data/network/request/CreateProfileRequest.kt index ba0b78a..fbafdfa 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/request/CreateProfileRequest.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/request/CreateProfileRequest.kt @@ -5,10 +5,9 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class CreateProfileRequest( - @Json(name = "verification_token") val verificationToken: String, @Json(name = "first_name") val firstName: String, @Json(name = "last_name") val lastName: String, - @Json(name = "username") val username: String, + @Json(name = "user_name") val username: String, @Json(name = "email") val email: String?, @Json(name = "password") val password: String ) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/VerificationTokenResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/VerificationTokenResponse.kt deleted file mode 100644 index ffb5677..0000000 --- a/app/src/main/java/com/example/mobileapp/data/network/response/VerificationTokenResponse.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.mobileapp.data.network.response - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class VerificationTokenResponse( - @Json(name = "verification_token") val verificationToken: String -) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/error/CreateProfileErrorResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/error/CreateProfileErrorResponse.kt index 4908dd5..575a050 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/response/error/CreateProfileErrorResponse.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/response/error/CreateProfileErrorResponse.kt @@ -2,14 +2,14 @@ package com.example.mobileapp.data.network.response.error import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.example.mobileapp.domain.Error @JsonClass(generateAdapter = true) data class CreateProfileErrorResponse( @Json(name = "non_field_errors") val nonFieldErrors: List?, - @Json(name = "verification_token") val verificationToken: List?, @Json(name = "first_name") val firstName: List?, @Json(name = "last_name") val lastName: List?, @Json(name = "email") val email: List?, @Json(name = "password") val password: List?, - @Json(name = "username") val username: List? + @Json(name = "user_name") val username: List? ) \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/error/RefreshAuthTokensErrorResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/error/RefreshAuthTokensErrorResponse.kt index 1450af5..fcc394a 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/response/error/RefreshAuthTokensErrorResponse.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/response/error/RefreshAuthTokensErrorResponse.kt @@ -2,6 +2,7 @@ package com.example.mobileapp.data.network.response.error import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.example.mobileapp.domain.Error @JsonClass(generateAdapter = true) data class RefreshAuthTokensErrorResponse( diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/error/SendRegistrationVerificationCodeErrorResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/error/SendRegistrationVerificationCodeErrorResponse.kt index 7bc5f14..150bca7 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/response/error/SendRegistrationVerificationCodeErrorResponse.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/response/error/SendRegistrationVerificationCodeErrorResponse.kt @@ -2,6 +2,7 @@ package com.example.mobileapp.data.network.response.error import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.example.mobileapp.domain.Error @JsonClass(generateAdapter = true) data class SendRegistrationVerificationCodeErrorResponse( diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/error/SignInWithEmailErrorResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/error/SignInWithEmailErrorResponse.kt index 669d21d..e8665c4 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/response/error/SignInWithEmailErrorResponse.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/response/error/SignInWithEmailErrorResponse.kt @@ -2,6 +2,7 @@ package com.example.mobileapp.data.network.response.error import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.example.mobileapp.domain.Error @JsonClass(generateAdapter = true) data class SignInWithEmailErrorResponse( diff --git a/app/src/main/java/com/example/mobileapp/data/network/response/error/VerifyRegistrationCodeErrorResponse.kt b/app/src/main/java/com/example/mobileapp/data/network/response/error/VerifyRegistrationCodeErrorResponse.kt index d5c88f4..cd887d5 100644 --- a/app/src/main/java/com/example/mobileapp/data/network/response/error/VerifyRegistrationCodeErrorResponse.kt +++ b/app/src/main/java/com/example/mobileapp/data/network/response/error/VerifyRegistrationCodeErrorResponse.kt @@ -2,6 +2,7 @@ package com.example.mobileapp.data.network.response.error import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.example.mobileapp.domain.Error @JsonClass(generateAdapter = true) data class VerifyRegistrationCodeErrorResponse( diff --git a/app/src/main/java/com/example/mobileapp/domain/Errors.kt b/app/src/main/java/com/example/mobileapp/domain/Errors.kt index ff6bb80..bd8a129 100644 --- a/app/src/main/java/com/example/mobileapp/domain/Errors.kt +++ b/app/src/main/java/com/example/mobileapp/domain/Errors.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Error( - @Json(name = "code") val code: ErrorCode, + @Json(name = "code") val code: Int, @Json(name = "message") val message: String ) diff --git a/app/src/main/java/com/example/mobileapp/domain/User.kt b/app/src/main/java/com/example/mobileapp/domain/User.kt index 125e521..1240247 100644 --- a/app/src/main/java/com/example/mobileapp/domain/User.kt +++ b/app/src/main/java/com/example/mobileapp/domain/User.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass data class User( @Json(name = "id") val id: Long, @Json(name = "user_name") val username: String, - @Json(name = "avatar") val avatarUrl: String?, // E.g. "https://mydomain.com/user_1_avatar.jpg" + @Json(name = "picture") var avatarUrl: String?, //media.zenfs.com/en-US/homerun/time_72/ad41bb42a3e7be9eb1e32a9f2097b80f", // E.g. "https://mydomain.com/user_1_avatar.jpg" @Json(name = "first_name") val firstName: String, @Json(name = "last_name") val lastName: String, @Json(name = "group_name") val groupName: String? diff --git a/app/src/main/java/com/example/mobileapp/interactor/AuthInteractor.kt b/app/src/main/java/com/example/mobileapp/interactor/AuthInteractor.kt index a1fce24..48c9c79 100644 --- a/app/src/main/java/com/example/mobileapp/interactor/AuthInteractor.kt +++ b/app/src/main/java/com/example/mobileapp/interactor/AuthInteractor.kt @@ -1,7 +1,9 @@ package com.example.mobileapp.interactor +import com.example.mobileapp.data.network.response.error.CreateProfileErrorResponse import com.example.mobileapp.data.network.response.error.SignInWithEmailErrorResponse import com.example.mobileapp.domain.AuthTokens +import com.example.mobileapp.domain.User import com.example.mobileapp.repository.AuthRepository import com.haroldadmin.cnradapter.NetworkResponse import kotlinx.coroutines.flow.Flow @@ -27,6 +29,23 @@ class AuthInteractor @Inject constructor( return response } + suspend fun signUpWithPersonalInfo( + firstName: String, + lastName: String, + username: String, + email: String, + password: String + ): NetworkResponse { + val response = authRepository.generateUserByEmailAndPersonalInfo(firstName, lastName, + username, email, password) + when (response) { + is NetworkResponse.Error -> { + Timber.e(response.error) + } + } + return response + } + suspend fun logout() { authRepository.saveAuthTokens(null) } diff --git a/app/src/main/java/com/example/mobileapp/interactor/ProfileInteractor.kt b/app/src/main/java/com/example/mobileapp/interactor/ProfileInteractor.kt new file mode 100644 index 0000000..ddec05f --- /dev/null +++ b/app/src/main/java/com/example/mobileapp/interactor/ProfileInteractor.kt @@ -0,0 +1,23 @@ +package com.example.mobileapp.interactor + +import android.content.res.Resources +import com.example.mobileapp.R +import com.example.mobileapp.domain.User +import com.example.mobileapp.repository.ProfileRepository +import com.haroldadmin.cnradapter.NetworkResponse +import javax.inject.Inject + +class ProfileInteractor @Inject constructor( + private val profileRepository: ProfileRepository +) { + + suspend fun getProfile() : NetworkResponse { + val response = profileRepository.getProfile() + when (response) { + is NetworkResponse.Success -> { + response.body.avatarUrl = response.body.avatarUrl ?: "https://s.yimg.com/ny/api/res/1.2/5rmN2CQI90DCby7uM2UefQ--/YXBwaWQ9aGlnaGxhbmRlcjt3PTEyMDA7aD04MDA-/https://s.yimg.com/uu/api/res/1.2/W1dJaYmRHV_PHKDIDmm__Q--~B/aD0xNDAwO3c9MjEwMDthcHBpZD15dGFjaHlvbg--/http://media.zenfs.com/en-US/homerun/time_72/ad41bb42a3e7be9eb1e32a9f2097b80f" + } + } + return response + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/interactor/UsersInteractor.kt b/app/src/main/java/com/example/mobileapp/interactor/UsersInteractor.kt index 50881b7..82cf84c 100644 --- a/app/src/main/java/com/example/mobileapp/interactor/UsersInteractor.kt +++ b/app/src/main/java/com/example/mobileapp/interactor/UsersInteractor.kt @@ -9,7 +9,14 @@ class UsersInteractor @Inject constructor( private val usersRepository: UsersRepository ) { - suspend fun loadUsers() : NetworkResponse, Unit> = - usersRepository.getUsers() + suspend fun loadUsers() : NetworkResponse, Unit> { + val response = usersRepository.getUsers() + when (response) { + is NetworkResponse.Success -> { + response.body.map { it.avatarUrl = it.avatarUrl ?: "https://s.yimg.com/ny/api/res/1.2/5rmN2CQI90DCby7uM2UefQ--/YXBwaWQ9aGlnaGxhbmRlcjt3PTEyMDA7aD04MDA-/https://s.yimg.com/uu/api/res/1.2/W1dJaYmRHV_PHKDIDmm__Q--~B/aD0xNDAwO3c9MjEwMDthcHBpZD15dGFjaHlvbg--/http://media.zenfs.com/en-US/homerun/time_72/ad41bb42a3e7be9eb1e32a9f2097b80f" } + } + } + return response + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/repository/AuthRepository.kt b/app/src/main/java/com/example/mobileapp/repository/AuthRepository.kt index 92ade11..d03f2bd 100644 --- a/app/src/main/java/com/example/mobileapp/repository/AuthRepository.kt +++ b/app/src/main/java/com/example/mobileapp/repository/AuthRepository.kt @@ -12,6 +12,7 @@ import com.example.mobileapp.data.persistent.LocalKeyValueStorage import com.example.mobileapp.di.AppCoroutineScope import com.example.mobileapp.di.IoCoroutineDispatcher import com.example.mobileapp.domain.AuthTokens +import com.example.mobileapp.domain.User import com.haroldadmin.cnradapter.NetworkResponse import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -77,17 +78,15 @@ class AuthRepository @Inject constructor( * Creates a user account in the system as a side effect. * @return access tokens with higher permissions for the new registered user */ - suspend fun generateAuthTokensByEmailAndPersonalInfo( - verificationToken: String, + suspend fun generateUserByEmailAndPersonalInfo( firstName: String, lastName: String, username: String, email: String, password: String - ): NetworkResponse { + ): NetworkResponse { return api.createProfile( CreateProfileRequest( - verificationToken, firstName, lastName, username, diff --git a/app/src/main/java/com/example/mobileapp/repository/ProfileRepository.kt b/app/src/main/java/com/example/mobileapp/repository/ProfileRepository.kt new file mode 100644 index 0000000..96ff28e --- /dev/null +++ b/app/src/main/java/com/example/mobileapp/repository/ProfileRepository.kt @@ -0,0 +1,15 @@ +package com.example.mobileapp.repository + +import com.example.mobileapp.Api +import com.example.mobileapp.domain.User +import com.haroldadmin.cnradapter.NetworkResponse +import javax.inject.Inject + +class ProfileRepository @Inject constructor( + private val api: Api +) { + + suspend fun getProfile() : NetworkResponse = + api.getProfile() + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationFragment.kt b/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationFragment.kt index 4a5e6a3..feb33ba 100644 --- a/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationFragment.kt @@ -2,15 +2,28 @@ package com.example.mobileapp.ui.emailconfirmation import android.content.res.Configuration import android.os.Bundle +import android.view.Gravity import android.view.View +import android.widget.Toast import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import com.example.mobileapp.R import com.example.mobileapp.ui.base.BaseFragment import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.mobileapp.data.network.response.error.CreateProfileErrorResponse import com.example.mobileapp.databinding.FragmentEmailConfirmationBinding +import com.example.mobileapp.ui.signup.SignUpViewModel +import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import timber.log.Timber + +@AndroidEntryPoint class EmailConfirmationFragment : BaseFragment(R.layout.fragment_email_confirmation) { private val viewBinding by viewBinding(FragmentEmailConfirmationBinding::bind) @@ -20,14 +33,14 @@ class EmailConfirmationFragment : BaseFragment(R.layout.fragment_email_confirmat override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewModel.sendCode() + + subscribeToVerificationState() + viewBinding.backButton.applyInsetter { type(statusBars = true) { margin() } } - viewBinding.toEmailButton.applyInsetter { - type(navigationBars = true) { margin() } - } - when (resources.configuration.orientation) { Configuration.ORIENTATION_LANDSCAPE -> { viewBinding.verifyButton.applyInsetter { @@ -36,10 +49,55 @@ class EmailConfirmationFragment : BaseFragment(R.layout.fragment_email_confirmat } } + viewBinding.sendNewCodeButton?.applyInsetter { + type(navigationBars = true) { margin() } + } + viewBinding.backButton.setOnClickListener { findNavController().popBackStack() } + + viewBinding.verifyButton.setOnClickListener { + viewModel.verifyCode(viewBinding.verificationCodeEditText?.getCode()) + } + + viewBinding.sendNewCodeButton?.setOnClickListener { + viewModel.sendCode() + } + } + + private fun subscribeToVerificationState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.verificationStateFlow().collect(::renderVerificationState) + } + } } + private fun renderVerificationState(verificationState : EmailConfirmationViewModel.VerificationState) { + when (verificationState) { + is EmailConfirmationViewModel.VerificationState.Pending -> { + viewBinding.sendNewCodeButton?.text = resources.getString( + R.string.email_confirmation_resend) + viewBinding.sendNewCodeButton?.isEnabled = true + } + is EmailConfirmationViewModel.VerificationState.Verified -> { + findNavController().navigate(R.id.signInFragment) + } + is EmailConfirmationViewModel.VerificationState.TimerTicking -> { + if (verificationState.timeRemainSeconds == 9L) { + val toast = Toast.makeText( context, "Ваш секретный код: ${verificationState.code}", + Toast.LENGTH_LONG) + toast.show() + } + viewBinding.sendNewCodeButton?.text = resources.getString( + R.string.email_confirmation_timer, verificationState.timeRemainSeconds) + viewBinding.sendNewCodeButton?.isEnabled = false + } + is EmailConfirmationViewModel.VerificationState.VerificationError -> { + Toast.makeText(context, verificationState.message, Toast.LENGTH_SHORT).show() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationViewModel.kt b/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationViewModel.kt index 1488547..c102be2 100644 --- a/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationViewModel.kt +++ b/app/src/main/java/com/example/mobileapp/ui/emailconfirmation/EmailConfirmationViewModel.kt @@ -1,7 +1,69 @@ package com.example.mobileapp.ui.emailconfirmation +import android.os.CountDownTimer +import android.widget.Toast +import androidx.lifecycle.viewModelScope import com.example.mobileapp.ui.base.BaseViewModel +import com.example.mobileapp.ui.signup.SignUpViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlin.random.Random -class EmailConfirmationViewModel : BaseViewModel() { +@HiltViewModel +class EmailConfirmationViewModel @Inject constructor() : BaseViewModel() { + sealed class VerificationState { + object Pending : VerificationState() + object Verified : VerificationState() + data class VerificationError(val message: String) : VerificationState() + class TimerTicking(val code : String, val timeRemainSeconds: Long) : VerificationState() + } + + private val _verificationStateFlow = MutableStateFlow(VerificationState.Pending) + private var _secretCode = "123456" + private val _randomizer = Random(1337) + + fun sendCode() { + _secretCode = _randomizer.nextInt(100000, 999999).toString() + object : CountDownTimer(10000, 1000) { + override fun onTick(msRemain: Long) { + viewModelScope.launch { + _verificationStateFlow.emit( + VerificationState.TimerTicking( + _secretCode, + TimeUnit.MILLISECONDS.toSeconds(msRemain) + ) + ) + } + } + + override fun onFinish() { + viewModelScope.launch { + _verificationStateFlow.emit(VerificationState.Pending) + } + } + }.start() + } + + fun verificationStateFlow(): Flow { + return _verificationStateFlow.asStateFlow() + } + + fun verifyCode(code : String?) { + viewModelScope.launch { + if (code == null) { + _verificationStateFlow.emit(VerificationState.VerificationError("Неверный код")) + } else { + if (code == _secretCode) + _verificationStateFlow.emit(VerificationState.Verified) + else + _verificationStateFlow.emit(VerificationState.VerificationError("Неверный код")) + } + } + } } diff --git a/app/src/main/java/com/example/mobileapp/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/example/mobileapp/ui/onboarding/OnboardingFragment.kt index 2861a4a..7b74cb2 100644 --- a/app/src/main/java/com/example/mobileapp/ui/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/onboarding/OnboardingFragment.kt @@ -3,6 +3,7 @@ package com.example.mobileapp.ui.onboarding import android.content.res.Configuration import android.graphics.Color import android.os.Bundle +import android.os.CountDownTimer import android.os.Handler import android.view.View import androidx.navigation.fragment.findNavController @@ -21,6 +22,7 @@ import com.google.android.material.tabs.TabLayoutMediator import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter import dev.chrisbanes.insetter.applyInsetter + class OnboardingFragment : BaseFragment(R.layout.fragment_onboarding) { val viewBinding by viewBinding(FragmentOnboardingBinding::bind) diff --git a/app/src/main/java/com/example/mobileapp/ui/profile/ProfileFragment.kt b/app/src/main/java/com/example/mobileapp/ui/profile/ProfileFragment.kt index 9a20a1f..81f87b3 100644 --- a/app/src/main/java/com/example/mobileapp/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/profile/ProfileFragment.kt @@ -3,6 +3,7 @@ package com.example.mobileapp.ui.profile import android.os.Bundle import android.view.View import android.widget.Toast +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -10,6 +11,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.example.mobileapp.R import com.example.mobileapp.ui.base.BaseFragment import by.kirich1409.viewbindingdelegate.viewBinding +import com.bumptech.glide.Glide import com.example.mobileapp.databinding.FragmentProfileBinding import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter @@ -24,6 +26,7 @@ class ProfileFragment : BaseFragment(R.layout.fragment_profile) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) subscribeToEvents() + subscribeToViewState() viewBinding.logoutButton.applyInsetter { type(statusBars = true) { margin() } } @@ -35,6 +38,14 @@ class ProfileFragment : BaseFragment(R.layout.fragment_profile) { } } + private fun subscribeToViewState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.viewState.collect(::renderViewState) + } + } + } + private fun subscribeToEvents() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -55,4 +66,38 @@ class ProfileFragment : BaseFragment(R.layout.fragment_profile) { } } + private fun renderViewState(viewState : ProfileViewModel.ViewState) { + when (viewState) { + is ProfileViewModel.ViewState.Loading -> { + with(viewBinding) { + listOf(firstName, lastName, groupName, avatarImageView, selectedUsername) + }.forEach { + it.isVisible = false + } + viewBinding.progressBar.isVisible = true + + } + is ProfileViewModel.ViewState.Data -> { + viewBinding.progressBar.isVisible = false + with(viewBinding) { + listOf(firstName, lastName, groupName, avatarImageView, selectedUsername) + }.forEach { + it.isVisible = true + } + + Glide.with(viewBinding.avatarImageView) + .load(viewState.user.avatarUrl) + .circleCrop() + .into(viewBinding.avatarImageView) + viewBinding.selectedFirstName.text = viewState.user.firstName + viewBinding.selectedLastName.text = viewState.user.lastName + viewBinding.selectedGroup.text = viewState.user.groupName + viewBinding.selectedUsername.text = viewState.user.username + if (viewState.user.groupName == null) { + viewBinding.groupName.isVisible = false + } + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/ui/profile/ProfileViewModel.kt b/app/src/main/java/com/example/mobileapp/ui/profile/ProfileViewModel.kt index d94cf6e..d12989b 100644 --- a/app/src/main/java/com/example/mobileapp/ui/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/example/mobileapp/ui/profile/ProfileViewModel.kt @@ -1,11 +1,17 @@ package com.example.mobileapp.ui.profile import androidx.lifecycle.viewModelScope +import com.example.mobileapp.domain.User import com.example.mobileapp.interactor.AuthInteractor +import com.example.mobileapp.interactor.ProfileInteractor import com.example.mobileapp.ui.base.BaseViewModel +import com.example.mobileapp.ui.userlist.UserListViewModel +import com.haroldadmin.cnradapter.NetworkResponse import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import timber.log.Timber @@ -13,10 +19,14 @@ import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( - private val authInteractor: AuthInteractor + private val authInteractor: AuthInteractor, + private val profileInteractor: ProfileInteractor ) : BaseViewModel() { private val _eventChannel = Channel(Channel.BUFFERED) + private val _viewState = MutableStateFlow(ViewState.Loading) + val viewState: Flow get() = _viewState.asStateFlow() + fun eventsFlow() : Flow { return _eventChannel.receiveAsFlow() @@ -33,8 +43,31 @@ class ProfileViewModel @Inject constructor( } } + init { + getProfile() + } + + fun getProfile() { + viewModelScope.launch { + _viewState.emit(ViewState.Loading) + when (val response = profileInteractor.getProfile()) { + is NetworkResponse.Success -> { + _viewState.emit(ViewState.Data(response.body)) + } + else -> { + // TODO: handle errors + } + } + + } + } + sealed class Event { data class LogoutError(val error: Throwable) : Event() } + sealed class ViewState { + object Loading : ViewState() + data class Data(val user: User) : ViewState() + } } diff --git a/app/src/main/java/com/example/mobileapp/ui/signin/SignInFragment.kt b/app/src/main/java/com/example/mobileapp/ui/signin/SignInFragment.kt index dc30fd4..51a44ad 100644 --- a/app/src/main/java/com/example/mobileapp/ui/signin/SignInFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/signin/SignInFragment.kt @@ -7,16 +7,26 @@ import android.view.View import android.view.animation.Animation import android.view.animation.AnimationUtils import android.view.animation.RotateAnimation +import android.widget.Toast import androidx.activity.OnBackPressedCallback +import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.viewBinding import com.example.mobileapp.ui.base.BaseFragment import com.example.mobileapp.R +import com.example.mobileapp.data.network.response.error.CreateProfileErrorResponse +import com.example.mobileapp.data.network.response.error.SignInWithEmailErrorResponse import com.example.mobileapp.databinding.FragmentSignInBinding import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import timber.log.Timber @AndroidEntryPoint class SignInFragment : BaseFragment(R.layout.fragment_sign_in) { @@ -40,6 +50,8 @@ class SignInFragment : BaseFragment(R.layout.fragment_sign_in) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + subscribeToActionState() + viewBinding.backButton.applyInsetter { type(statusBars = true) { margin() } } @@ -84,6 +96,51 @@ class SignInFragment : BaseFragment(R.layout.fragment_sign_in) { viewBinding.mcsLogoImageView.startAnimation(animation) } + private fun subscribeToActionState() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.signInActionStateFlow().collect(::renderActionState) + } + } + } + + private fun renderActionState(actionState: SignInViewModel.SignInActionState) { + val textInputLayouts = + with (viewBinding) { + listOf(emailTextInputLayout, + passwordTextInputLayout) + } + textInputLayouts.forEach { it.error = null } + when (actionState) { + is SignInViewModel.SignInActionState.Pending -> { } + is SignInViewModel.SignInActionState.Loading -> { } + is SignInViewModel.SignInActionState.ServerError -> { + val error = actionState.e.body ?: SignInWithEmailErrorResponse(null, + null, null) + val errors = + with(error) { + listOf(email, password) + } + + (errors zip textInputLayouts).forEach { (error, inputLayout) -> + if (error != null) { + if (error.isNotEmpty()) { + inputLayout.error = error[0].message + } + } + } + } + is SignInViewModel.SignInActionState.NetworkError -> { + Toast.makeText(context, "Плохое интернет соединение. Проверьте подключение и повторите", + Toast.LENGTH_SHORT).show() + } + else -> { + Toast.makeText(context, "Упс, что-то пошло не так. Попробуйте еще раз", + Toast.LENGTH_SHORT).show() + } + } + } + private fun subscribeToFromFields() { decideSignInButtonEnabledState( email = viewBinding.emailEditText.text?.toString(), diff --git a/app/src/main/java/com/example/mobileapp/ui/signup/SignUpFragment.kt b/app/src/main/java/com/example/mobileapp/ui/signup/SignUpFragment.kt index b307be8..433efd9 100644 --- a/app/src/main/java/com/example/mobileapp/ui/signup/SignUpFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/signup/SignUpFragment.kt @@ -13,9 +13,11 @@ import android.text.style.ClickableSpan import android.view.View import android.widget.CheckBox import android.widget.TextView +import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.core.text.buildSpannedString import androidx.core.text.inSpans +import androidx.core.view.isVisible import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -24,13 +26,18 @@ import androidx.navigation.fragment.findNavController import by.kirich1409.viewbindingdelegate.viewBinding import com.example.mobileapp.ui.base.BaseFragment import com.example.mobileapp.R +import com.example.mobileapp.data.network.response.error.CreateProfileErrorResponse +import com.example.mobileapp.data.network.response.error.SignInWithEmailErrorResponse import com.example.mobileapp.databinding.FragmentSignUpBinding import com.example.mobileapp.util.getSpannedString +import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.insetter.applyInsetter import kotlinx.coroutines.launch import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect +import timber.log.Timber +@AndroidEntryPoint class SignUpFragment : BaseFragment(R.layout.fragment_sign_up) { private val viewModel: SignUpViewModel by viewModels() @@ -51,7 +58,8 @@ class SignUpFragment : BaseFragment(R.layout.fragment_sign_up) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - subscribeToEvents() + + subscribeToActionState() viewBinding.backButton.applyInsetter { type(statusBars = true) { margin() } @@ -86,9 +94,9 @@ class SignUpFragment : BaseFragment(R.layout.fragment_sign_up) { } viewBinding.signUpButton.setOnClickListener { viewModel.signUp( - firstname = viewBinding.firstnameEditText.text?.toString() ?: "", - lastname = viewBinding.lastnameEditText.text?.toString() ?: "", - nickname = viewBinding.nicknameEditText.text?.toString() ?: "", + firstName = viewBinding.firstnameEditText.text?.toString() ?: "", + lastName = viewBinding.lastnameEditText.text?.toString() ?: "", + username = viewBinding.nicknameEditText.text?.toString() ?: "", email = viewBinding.emailEditText.text?.toString() ?: "", password = viewBinding.passwordEditText.text?.toString() ?: "" ) @@ -102,22 +110,54 @@ class SignUpFragment : BaseFragment(R.layout.fragment_sign_up) { subscribeToFormFields() } - private fun subscribeToEvents() { + private fun subscribeToActionState() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.signUpActionStateFlow().collect(::renderActionState) + } + } + } - viewModel.eventsFlow().collect { event -> - when (event) { - is SignUpViewModel.Event.SignUpEmailConfirmationRequired -> { - findNavController().navigate(R.id.emailConfirmationFragment) - } - else -> { - // Do nothing. + private fun renderActionState(actionState: SignUpViewModel.SignUpActionState) { + val textInputLayouts = + with (viewBinding) { + listOf(firstnameTextInputLayout, + lastnameTextInputLayout, + emailTextInputLayout, + passwordTextInputLayout, + nicknameTextInputLayout) + } + textInputLayouts.forEach { it.error = null } + + when (actionState) { + is SignUpViewModel.SignUpActionState.Pending -> {} + is SignUpViewModel.SignUpActionState.Loading -> {} + is SignUpViewModel.SignUpActionState.ServerError -> { + val error = actionState.e.body ?: CreateProfileErrorResponse(null, + null, null, null, null, null) + val errors = + with(error) { + listOf(firstName, lastName, email, password, username) + } + (errors zip textInputLayouts).forEach { (error, inputLayout) -> + Timber.d(error.toString()) + if (error != null) { + if (error.size > 0) { + inputLayout.error = error[0].message } } } } + is SignUpViewModel.SignUpActionState.ServerSignInError -> {} + is SignUpViewModel.SignUpActionState.NetworkError -> { + Toast.makeText(context, "Плохое интернет соединение. Проверьте подключение и повторите", + Toast.LENGTH_SHORT).show() + } + else -> { + Toast.makeText(context, "Упс, что-то пошло не так. Попробуйте еще раз", + Toast.LENGTH_SHORT).show() + } } } diff --git a/app/src/main/java/com/example/mobileapp/ui/signup/SignUpViewModel.kt b/app/src/main/java/com/example/mobileapp/ui/signup/SignUpViewModel.kt index 860026e..8a1f3bf 100644 --- a/app/src/main/java/com/example/mobileapp/ui/signup/SignUpViewModel.kt +++ b/app/src/main/java/com/example/mobileapp/ui/signup/SignUpViewModel.kt @@ -1,48 +1,94 @@ package com.example.mobileapp.ui.signup import androidx.lifecycle.viewModelScope +import com.example.mobileapp.data.network.response.error.CreateProfileErrorResponse +import com.example.mobileapp.data.network.response.error.SignInWithEmailErrorResponse +import com.example.mobileapp.interactor.AuthInteractor import com.example.mobileapp.repository.AuthRepositoryOld import com.example.mobileapp.ui.base.BaseViewModel +import com.example.mobileapp.ui.signin.SignInViewModel +import com.haroldadmin.cnradapter.NetworkResponse +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch +import timber.log.Timber import java.lang.Exception +import javax.inject.Inject -class SignUpViewModel : BaseViewModel() { +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val authInteractor: AuthInteractor +) : BaseViewModel() { - private val _eventChannel = Channel(Channel.BUFFERED) + private val _signUpActionStateFlow = MutableStateFlow(SignUpActionState.Pending) - fun eventsFlow(): Flow { - return _eventChannel.receiveAsFlow() + fun signUpActionStateFlow(): Flow { + return _signUpActionStateFlow.asStateFlow() } - fun signUp( - firstname: String, - lastname: String, - nickname: String, - email: String, - password: String - ) { + private suspend fun signIn(email: String, password: String) { + _signUpActionStateFlow.emit(SignUpActionState.Loading) + try { + when (val response = authInteractor.signInWithEmail(email, password)) { + is NetworkResponse.Success -> { + _signUpActionStateFlow.emit(SignUpActionState.Pending) + } + is NetworkResponse.ServerError -> { + _signUpActionStateFlow.emit(SignUpActionState.ServerSignInError(response)) + } + is NetworkResponse.NetworkError -> { + _signUpActionStateFlow.emit(SignUpActionState.NetworkError(response)) + } + is NetworkResponse.UnknownError -> { + _signUpActionStateFlow.emit(SignUpActionState.UnknownError(response)) + } + } + } catch (error: Throwable) { + Timber.e(error) + _signUpActionStateFlow.emit(SignUpActionState.UnknownError(NetworkResponse.UnknownError(error))) + } + } + + fun signUp(firstName: String, + lastName: String, + username: String, + email: String, + password: String) { viewModelScope.launch { + _signUpActionStateFlow.emit(SignUpActionState.Loading) try { - AuthRepositoryOld.signUp( - firstname, - lastname, - nickname, - email, - password - ) - // _eventChannel.send(Event.SignUpSuccess) - _eventChannel.send(Event.SignUpEmailConfirmationRequired) - } catch (error: Exception) { - _eventChannel.send(Event.SignUpEmailConfirmationRequired) + when (val response = authInteractor.signUpWithPersonalInfo(firstName, lastName, username, email, password)) { + is NetworkResponse.Success -> { + _signUpActionStateFlow.emit(SignUpActionState.Pending) + } + is NetworkResponse.ServerError -> { + _signUpActionStateFlow.emit(SignUpActionState.ServerError(response)) + } + is NetworkResponse.NetworkError -> { + _signUpActionStateFlow.emit(SignUpActionState.NetworkError(response)) + } + is NetworkResponse.UnknownError -> { + _signUpActionStateFlow.emit(SignUpActionState.UnknownError(response)) + } + } + } catch (error: Throwable) { + Timber.e(error) + _signUpActionStateFlow.emit(SignUpActionState.UnknownError(NetworkResponse.UnknownError(error))) } + } } - sealed class Event { - object SignUpSuccess : Event() - object SignUpEmailConfirmationRequired : Event() + sealed class SignUpActionState { + object Pending : SignUpActionState() + object Loading : SignUpActionState() + data class ServerError(val e: NetworkResponse.ServerError) : SignUpActionState() + data class NetworkError(val e: NetworkResponse.NetworkError) : SignUpActionState() + data class ServerSignInError(val e: NetworkResponse.ServerError) : SignUpActionState() + data class UnknownError(val e: NetworkResponse.UnknownError) : SignUpActionState() } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mobileapp/ui/userlist/UserListFragment.kt b/app/src/main/java/com/example/mobileapp/ui/userlist/UserListFragment.kt index 9a89c68..f95bb75 100644 --- a/app/src/main/java/com/example/mobileapp/ui/userlist/UserListFragment.kt +++ b/app/src/main/java/com/example/mobileapp/ui/userlist/UserListFragment.kt @@ -29,9 +29,6 @@ class UserListFragment : BaseFragment(R.layout.fragment_user_list) { setupRecyclerView() subscribeToViewState() - viewBinding.usersRecyclerView.applyInsetter { - type(statusBars = true) { margin() } - } viewBinding.pullToRefreshLayout.applyInsetter { type(statusBars = true) { margin() } } @@ -59,10 +56,12 @@ class UserListFragment : BaseFragment(R.layout.fragment_user_list) { private fun renderViewState(viewState: UserListViewModel.ViewState) { when (viewState) { is UserListViewModel.ViewState.Loading -> { + viewBinding.pullToRefreshLayout.isRefreshing = true viewBinding.usersRecyclerView.isVisible = false viewBinding.errorLayout.isVisible = false } is UserListViewModel.ViewState.Data -> { + viewBinding.pullToRefreshLayout.isRefreshing = false viewBinding.usersRecyclerView.isVisible = true viewBinding.errorLayout.isVisible = false @@ -72,12 +71,14 @@ class UserListFragment : BaseFragment(R.layout.fragment_user_list) { } } is UserListViewModel.ViewState.Error -> { + viewBinding.pullToRefreshLayout.isRefreshing = false viewBinding.usersRecyclerView.isVisible = false viewBinding.errorLayout.isVisible = true viewBinding.errorText.text = resources.getString(R.string.userlist_error_message) } is UserListViewModel.ViewState.EmptyList -> { + viewBinding.pullToRefreshLayout.isRefreshing = false viewBinding.usersRecyclerView.isVisible = false viewBinding.errorLayout.isVisible = true diff --git a/app/src/main/res/layout/fragment_email_confirmation.xml b/app/src/main/res/layout/fragment_email_confirmation.xml index 3ffa3a3..4e80146 100644 --- a/app/src/main/res/layout/fragment_email_confirmation.xml +++ b/app/src/main/res/layout/fragment_email_confirmation.xml @@ -26,6 +26,7 @@ android:layout_marginEnd="48dp"/>