From 836e24247ed391af346bf6989578863338945bab Mon Sep 17 00:00:00 2001 From: "Z. Charles Dziura" Date: Sat, 23 Nov 2024 11:12:58 -0500 Subject: [PATCH] Mostly complete the user registration flow --- .../debtpirate/app/host/MainActivity.kt | 23 ++- .../app/screen/auth/di/AuthScreensDiModule.kt | 11 +- .../auth/presentation/confirm/Confirmation.kt | 89 ---------- .../confirm/ConfirmationScreen.kt | 167 ++++++++++++++++++ .../confirm/ConfirmationScreenViewModel.kt | 36 ++++ .../login/LoginScreenViewModel.kt | 2 +- .../register/RegistrationScreenViewModel.kt | 4 +- ...SubmitAccountRegistrationRequestUseCase.kt | 2 +- ...SubmitNewUserVerificationRequestUseCase.kt | 22 +++ .../data/remote/endpoint/UserEndpoint.kt | 4 +- .../debtpirate/navigation/Destination.kt | 15 +- 11 files changed, 263 insertions(+), 112 deletions(-) delete mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/Confirmation.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreen.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreenViewModel.kt create mode 100644 app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitNewUserVerificationRequestUseCase.kt diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt index c29d494..128a6d5 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/host/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import dagger.hilt.android.AndroidEntryPoint +import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationData import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm.ConfirmationScreen import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreen import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen @@ -88,16 +89,20 @@ class MainActivity : ComponentActivity() { } ) { navigation( - startDestination = Destination.LoginScreen + startDestination = Destination.AuthLogin ) { - composable { LoginScreen() } - composable { RegistrationScreen() } - composable { - val (emailAddress, _) = it.toRoute() + composable { LoginScreen() } + composable { RegistrationScreen() } + composable { + val (emailAddress) = it.toRoute() ConfirmationScreen( - emailAddress + ConfirmationData.RegistrationConfirmationData(emailAddress) ) } + composable { + val (userId, verificationToken) = it.toRoute() + ConfirmationScreen(ConfirmationData.UserConfirmationData(userId, verificationToken)) + } } } } @@ -127,11 +132,11 @@ class MainActivity : ComponentActivity() { } } - private fun handleNewUserVerificationIntent(userId: Int, sessionToken: String) { - Log.d("DebtPirate::MainActivity", "User ID: $userId, Session Token: $sessionToken") + private fun handleNewUserVerificationIntent(userId: Int, verificationToken: String) { + Log.d("DebtPirate::MainActivity", "User ID: $userId, Session Token: $verificationToken") lifecycleScope.launch { - navigator.navigate(Destination.RegistrationConfirmationCheckEmailScreen(comingFromEmail = true)) + navigator.navigate(Destination.AuthUserConfirmation(userId, verificationToken)) } } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/di/AuthScreensDiModule.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/di/AuthScreensDiModule.kt index 3a83878..dc22a80 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/di/AuthScreensDiModule.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/di/AuthScreensDiModule.kt @@ -7,6 +7,7 @@ import dagger.hilt.android.components.ViewModelComponent import dagger.hilt.android.scopes.ViewModelScoped import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitAccountRegistrationRequestUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitLoginCredentialsUseCase +import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitNewUserVerificationRequestUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateLoginCredentialsUseCase import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.ValidateNewAccountRegistrationUseCase import ing.bikeshedengineer.debtpirate.data.repository.AuthRepository @@ -32,7 +33,11 @@ object AuthScreensDiModule { @Provides @ViewModelScoped - fun provideSubmitAccountRegistrationUseCase(userRepository: UserRepository): SubmitAccountRegistrationRequestUseCase { - return SubmitAccountRegistrationRequestUseCase(userRepository) - } + fun provideSubmitAccountRegistrationUseCase(userRepository: UserRepository): SubmitAccountRegistrationRequestUseCase = + SubmitAccountRegistrationRequestUseCase(userRepository) + + @Provides + @ViewModelScoped + fun provideSubmitNewUserVerificationUseCase(userRepository: UserRepository): SubmitNewUserVerificationRequestUseCase = + SubmitNewUserVerificationRequestUseCase(userRepository) } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/Confirmation.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/Confirmation.kt deleted file mode 100644 index 55f69ef..0000000 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/Confirmation.kt +++ /dev/null @@ -1,89 +0,0 @@ -package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.MarkEmailRead -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp - -@Composable() -fun ConfirmationScreen(emailAddress: String? = null, redirectedFromEmailConfirmation: Boolean = false) { - Scaffold( - modifier = Modifier.fillMaxSize() - ) { padding -> - Column(modifier = Modifier.padding(padding).padding(PaddingValues(4.dp))) { - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Icon( - Icons.Outlined.MarkEmailRead, - null, - modifier = Modifier - .align(Alignment.Center) - .size(96.dp) - ) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .weight(3f) - ) { - Text( - "Confirmation Sent!", - style = MaterialTheme.typography.displaySmall, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - - if (emailAddress != null) { - Text( - text = buildAnnotatedString { - append("We sent your confirmation email to ") - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold - ) - ) { - append(emailAddress) - } - }, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(PaddingValues(top = 16.dp)) - - ) - - Text( - "Follow the instructions listed in that email to finish registering your new account.", - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(PaddingValues(top = 8.dp)) - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreen.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreen.kt new file mode 100644 index 0000000..aa74e90 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreen.kt @@ -0,0 +1,167 @@ +package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.outlined.MarkEmailRead +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +sealed class ConfirmationData { + data class RegistrationConfirmationData(val emailAddress: String) : ConfirmationData() + data class UserConfirmationData(val userId: Int, val verificationToken: String) : ConfirmationData() +} + +@Composable() +fun ConfirmationScreen( + data: ConfirmationData, + viewModel: ConfirmationScreenViewModel = hiltViewModel() +) { + Scaffold( + modifier = Modifier.fillMaxSize() + ) { padding -> + when (data) { + is ConfirmationData.RegistrationConfirmationData -> { + val (emailAddress) = data + RegistrationConfirmationScreen(emailAddress, modifier = Modifier.padding(padding)) + + } + + is ConfirmationData.UserConfirmationData -> { + val (userId, verificationToken) = data + LaunchedEffect(userId, verificationToken) { + viewModel.submitNewUserVerificationRequest(userId, verificationToken) + } + + val isLoading = viewModel.isLoading.collectAsState() + UserVerifiedScreen(isLoading, modifier = Modifier.padding(padding)) + } + } + } +} + +@Composable +private fun RegistrationConfirmationScreen(emailAddress: String, modifier: Modifier = Modifier) { + Column(modifier = modifier.padding(PaddingValues(4.dp))) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Icon( + Icons.Outlined.MarkEmailRead, + null, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp) + ) + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(3f) + ) { + Text( + "Confirmation Sent!", + style = MaterialTheme.typography.displaySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = buildAnnotatedString { + append("We sent your confirmation email to ") + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + ) { + append(emailAddress) + } + }, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(PaddingValues(top = 16.dp)) + + ) + + Text( + "Follow the instructions listed in that email to finish registering your new account.", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(PaddingValues(top = 8.dp)) + ) + } + } +} + +@Composable +private fun UserVerifiedScreen(isLoading: State, modifier: Modifier = Modifier) { + Column(modifier = modifier.padding(PaddingValues(4.dp))) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + if (isLoading.value) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp) + ) + } else { + Icon( + Icons.Outlined.CheckCircle, + null, + modifier = Modifier + .align(Alignment.Center) + .size(96.dp) + ) + + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .weight(3f) + ) { + if (!isLoading.value) { + Text( + "You're all set!", + style = MaterialTheme.typography.displaySmall, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreenViewModel.kt new file mode 100644 index 0000000..727cd97 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/ConfirmationScreenViewModel.kt @@ -0,0 +1,36 @@ +package ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.confirm + +import android.util.Log +import androidx.datastore.core.DataStore +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import ing.bikeshedengineer.debtpirate.PrefsDataStore +import ing.bikeshedengineer.debtpirate.app.screen.auth.usecase.SubmitNewUserVerificationRequestUseCase +import ing.bikeshedengineer.debtpirate.navigation.Navigator +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ConfirmationScreenViewModel @Inject constructor( + private val navigator: Navigator, + private val prefsStore: DataStore, + private val verifyNewUser: SubmitNewUserVerificationRequestUseCase +) : ViewModel() { + private var _isLoading = MutableStateFlow(true) + val isLoading = _isLoading.asStateFlow() + + fun submitNewUserVerificationRequest(userId: Int, verificationToken: String) { + viewModelScope.launch { + try { + val response = verifyNewUser(userId, verificationToken) + Log.d("ConfirmationScreenViewModel", "$response") + _isLoading.value = false + } catch (err: Throwable) { + // TODO... + } + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt index d825a8b..47ad9bd 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/login/LoginScreenViewModel.kt @@ -114,7 +114,7 @@ class LoginScreenViewModel @Inject constructor( fun onRegisterButtonClick() { viewModelScope.launch { - navigator.navigate(destination = Destination.RegistrationScreen) + navigator.navigate(destination = Destination.AuthRegistration) } } } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt index d7b0c41..e098b2f 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/register/RegistrationScreenViewModel.kt @@ -30,8 +30,8 @@ class RegistrationScreenViewModel @Inject constructor( fun navigateToConfirmationScreen(emailAddress: String) { viewModelScope.launch { - navigator.navigate(Destination.RegistrationConfirmationCheckEmailScreen(emailAddress)) { - popUpTo(Destination.LoginScreen) { + navigator.navigate(Destination.AuthRegistrationConfirmation(emailAddress)) { + popUpTo(Destination.AuthLogin) { inclusive = true } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitAccountRegistrationRequestUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitAccountRegistrationRequestUseCase.kt index 4142f35..85ce078 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitAccountRegistrationRequestUseCase.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitAccountRegistrationRequestUseCase.kt @@ -19,7 +19,7 @@ class SubmitAccountRegistrationRequestUseCase(private val userRepository: UserRe return response } catch (err: Throwable) { // TODO... - Log.e("RegistrationScreen", "$err") + Log.e("SubmitAccountRegistrationRequestUseCase", "$err") throw err } } diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitNewUserVerificationRequestUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitNewUserVerificationRequestUseCase.kt new file mode 100644 index 0000000..2b1bba5 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitNewUserVerificationRequestUseCase.kt @@ -0,0 +1,22 @@ +package ing.bikeshedengineer.debtpirate.app.screen.auth.usecase + +import android.util.Log +import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetRequest +import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse +import ing.bikeshedengineer.debtpirate.data.repository.UserRepository + +class SubmitNewUserVerificationRequestUseCase(private val userRepository: UserRepository) { + suspend operator fun invoke(userId: Int, verificationToken: String): UserVerificationGetResponse { + val request = UserVerificationGetRequest(userId, verificationToken) + + try { + val response = userRepository.verifyUserGetRequest(request) + Log.d("SubmitAccountRegistrationRequestUseCase", "User verification successful! $response") + return response + } catch (err: Throwable) { + // TODO... + Log.e("SubmitNewUserVerificationRequestUseCase", "$err") + throw err + } + } +} \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/endpoint/UserEndpoint.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/endpoint/UserEndpoint.kt index 1b7b82c..6e94687 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/endpoint/UserEndpoint.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/endpoint/UserEndpoint.kt @@ -6,10 +6,10 @@ import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResp import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse import retrofit2.Response import retrofit2.http.Body -import retrofit2.http.Field import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.Path +import retrofit2.http.Query interface UserEndpoint { @POST("user") @@ -18,6 +18,6 @@ interface UserEndpoint { @GET("user/{userId}/verify") suspend fun userVerificationGetRequest( @Path("userId") userId: Int, - @Field("t") verificationToken: String + @Query("t") verificationToken: String ): Response> } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Destination.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Destination.kt index bdcad8a..1fce751 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Destination.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/navigation/Destination.kt @@ -9,14 +9,19 @@ sealed interface Destination { data object AuthGraph : Destination @Serializable - data object LoginScreen : Destination + data object AuthLogin : Destination @Serializable - data object RegistrationScreen : Destination + data object AuthRegistration : Destination @Serializable - data class RegistrationConfirmationCheckEmailScreen( - val emailAddress: String? = null, - val comingFromEmail: Boolean = false + data class AuthRegistrationConfirmation( + val emailAddress: String + ) : Destination + + @Serializable + data class AuthUserConfirmation( + val userId: Int, + val verificationToken: String ) : Destination } \ No newline at end of file