diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index e51e3fa..913b745 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -20,9 +20,19 @@ android:theme="@style/Theme.DebtPirate"> - + + + + + + + + + + + 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 d96013c..5a2dc53 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 @@ -1,8 +1,10 @@ package ing.bikeshedengineer.debtpirate.app.host +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity -import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedContentTransitionScope @@ -18,7 +20,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable 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.ConfirmationScreen import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.login.LoginScreen import ing.bikeshedengineer.debtpirate.app.screen.auth.presentation.register.RegistrationScreen import ing.bikeshedengineer.debtpirate.navigation.Destination @@ -37,6 +41,9 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + handleIntent(intent) + enableEdgeToEdge() setContent { val navController = rememberNavController() @@ -79,15 +86,50 @@ class MainActivity : ComponentActivity() { } ) { navigation( - startDestination = Destination.LoginScreen + startDestination = Destination.RegistrationConfirmationCheckEmailScreen( + emailAddress = "zachary@dziura.email" + ) ) { composable() { LoginScreen() } composable() { RegistrationScreen() } + composable() { + val (emailAddress, _) = it.toRoute() + ConfirmationScreen( + emailAddress + ) + } } } } } } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent) { + val action: String? = intent.action + val actionUri: Uri? = intent.data + + if (Intent.ACTION_VIEW == action) { + when (actionUri?.lastPathSegment) { + "verify" -> { + val userId = actionUri.getQueryParameter("u")!!.toInt() + val verificationToken = actionUri.getQueryParameter("t")!! + + handleNewUserVerificationIntent(userId, verificationToken) + } + + else -> {} + } + } + } + + private fun handleNewUserVerificationIntent(userId: Int, sessionToken: String) { + Log.d("DebtPirate::MainActivity", "User ID: $userId, Session Token: $sessionToken") + } } @Composable 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 new file mode 100644 index 0000000..55f69ef --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/presentation/confirm/Confirmation.kt @@ -0,0 +1,89 @@ +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/usecase/SubmitAccountRegistrationRequestUseCase.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/app/screen/auth/usecase/SubmitAccountRegistrationRequestUseCase.kt index 8481240..4142f35 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 @@ -14,7 +14,7 @@ class SubmitAccountRegistrationRequestUseCase(private val userRepository: UserRe val request = UserCreatePostRequest(emailAddress, name, password) try { - val response = userRepository.submitCreateUserRequest(request) + val response = userRepository.createUserPostRequest(request) Log.d("RegistrationScreen", "Account registration successful! $response") return response } catch (err: Throwable) { 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 cf44bfd..1b7b82c 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 @@ -3,11 +3,21 @@ package ing.bikeshedengineer.debtpirate.data.remote.endpoint import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse +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 interface UserEndpoint { @POST("user") - suspend fun submitUserPostRequest(@Body request: UserCreatePostRequest): Response> + suspend fun createUserPostRequest(@Body request: UserCreatePostRequest): Response> + + @GET("user/{userId}/verify") + suspend fun userVerificationGetRequest( + @Path("userId") userId: Int, + @Field("t") verificationToken: String + ): Response> } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetRequest.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetRequest.kt new file mode 100644 index 0000000..63ac7b4 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetRequest.kt @@ -0,0 +1,3 @@ +package ing.bikeshedengineer.debtpirate.data.remote.model.user + +data class UserVerificationGetRequest(val userId: Int, val verificationToken: String) diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetResponse.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetResponse.kt new file mode 100644 index 0000000..e6e5872 --- /dev/null +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/remote/model/user/UserVerificationGetResponse.kt @@ -0,0 +1,18 @@ +package ing.bikeshedengineer.debtpirate.data.remote.model.user + +import com.google.gson.annotations.JsonAdapter +import ing.bikeshedengineer.debtpirate.domain.adapter.OffsetDateTimeAdapter +import java.time.OffsetDateTime + +data class UserVerificationGetResponse( + val userId: Int, + val session: UserVerificationGetResponseTokenData, + val auth: UserVerificationGetResponseTokenData +) + +data class UserVerificationGetResponseTokenData( + val token: String, + @JsonAdapter(OffsetDateTimeAdapter::class) val expiresAt: OffsetDateTime +) + + diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/repository/UserRepository.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/repository/UserRepository.kt index 0d7e3c2..4e15104 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/repository/UserRepository.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/data/repository/UserRepository.kt @@ -2,7 +2,10 @@ package ing.bikeshedengineer.debtpirate.data.repository import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse +import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetRequest +import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserVerificationGetResponse interface UserRepository { - suspend fun submitCreateUserRequest(request: UserCreatePostRequest): UserCreatePostResponse + suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse + suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse } \ No newline at end of file diff --git a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/UserRepositoryImpl.kt b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/UserRepositoryImpl.kt index 591e419..4cc6977 100644 --- a/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/UserRepositoryImpl.kt +++ b/app/app/src/main/java/ing/bikeshedengineer/debtpirate/domain/repository/UserRepositoryImpl.kt @@ -7,6 +7,8 @@ import ing.bikeshedengineer.debtpirate.data.remote.ApiResponse import ing.bikeshedengineer.debtpirate.data.remote.endpoint.UserEndpoint import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostRequest import ing.bikeshedengineer.debtpirate.data.remote.model.user.UserCreatePostResponse +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 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -15,13 +17,31 @@ import retrofit2.Retrofit class UserRepositoryImpl(httpClient: Retrofit) : UserRepository { private val userEndpoint: UserEndpoint = httpClient.create(UserEndpoint::class.java) - override suspend fun submitCreateUserRequest(request: UserCreatePostRequest): UserCreatePostResponse { + override suspend fun createUserPostRequest(request: UserCreatePostRequest): UserCreatePostResponse { return withContext(Dispatchers.IO) { - val response = userEndpoint.submitUserPostRequest(request) + val response = userEndpoint.createUserPostRequest(request) + + if (response.isSuccessful) { + val body = response.body() + return@withContext body!!.data!! + } else { + val gson = Gson() + val errorType = object : TypeToken>() {}.type + val body = + gson.fromJson>(response.errorBody()!!.charStream(), errorType) + + throw Throwable(body!!.error!!) + } + } + } + + override suspend fun verifyUserGetRequest(request: UserVerificationGetRequest): UserVerificationGetResponse { + return withContext(Dispatchers.IO) { + val (userId, verificationToken) = request + val response = userEndpoint.userVerificationGetRequest(userId, verificationToken) if (response.isSuccessful) { val body = response.body() - Log.d("Registration", "$body") return@withContext body!!.data!! } else { val gson = Gson() 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 b6e1a22..bdcad8a 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 @@ -13,4 +13,10 @@ sealed interface Destination { @Serializable data object RegistrationScreen : Destination + + @Serializable + data class RegistrationConfirmationCheckEmailScreen( + val emailAddress: String? = null, + val comingFromEmail: Boolean = false + ) : Destination } \ No newline at end of file diff --git a/app/gradle/libs.versions.toml b/app/gradle/libs.versions.toml index d0f7bf2..f928cbb 100644 --- a/app/gradle/libs.versions.toml +++ b/app/gradle/libs.versions.toml @@ -2,7 +2,7 @@ activityCompose = "1.9.3" agp = "8.7.2" appcompat = "1.7.0" -composeBom = "2024.10.01" +composeBom = "2024.11.00" coreKtx = "1.15.0" datastore = "1.1.1" espressoCore = "3.6.1" @@ -17,8 +17,8 @@ lifecycleRuntimeKtx = "2.8.7" lifecycleViewModelKtx = "2.8.7" lifecycleViewmodelCompose = "2.8.7" material = "1.12.0" -material3 = "1.4.0-alpha03" -navigation = "2.8.3" +material3 = "1.4.0-alpha04" +navigation = "2.8.4" protobuf = "0.9.4" protoLite = "3.21.11" okhttp = "4.10.0"