Create confirmation screen

This commit is contained in:
Z. Charles Dziura 2024-11-19 15:13:20 -05:00
parent f3eb73ee42
commit f107994f0b
11 changed files with 213 additions and 12 deletions

View file

@ -20,9 +20,19 @@
android:theme="@style/Theme.DebtPirate">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="debtpirate.app" />
<data android:pathPattern="/verify" />
</intent-filter>
</activity>
</application>

View file

@ -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<Destination.AuthGraph>(
startDestination = Destination.LoginScreen
startDestination = Destination.RegistrationConfirmationCheckEmailScreen(
emailAddress = "zachary@dziura.email"
)
) {
composable<Destination.LoginScreen>() { LoginScreen() }
composable<Destination.RegistrationScreen>() { RegistrationScreen() }
composable<Destination.RegistrationConfirmationCheckEmailScreen>() {
val (emailAddress, _) = it.toRoute<Destination.RegistrationConfirmationCheckEmailScreen>()
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

View file

@ -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))
)
}
}
}
}
}

View file

@ -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) {

View file

@ -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<ApiResponse<UserCreatePostResponse>>
suspend fun createUserPostRequest(@Body request: UserCreatePostRequest): Response<ApiResponse<UserCreatePostResponse>>
@GET("user/{userId}/verify")
suspend fun userVerificationGetRequest(
@Path("userId") userId: Int,
@Field("t") verificationToken: String
): Response<ApiResponse<UserVerificationGetResponse>>
}

View file

@ -0,0 +1,3 @@
package ing.bikeshedengineer.debtpirate.data.remote.model.user
data class UserVerificationGetRequest(val userId: Int, val verificationToken: String)

View file

@ -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
)

View file

@ -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
}

View file

@ -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<ApiResponse<Unit>>() {}.type
val body =
gson.fromJson<ApiResponse<Unit>>(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()

View file

@ -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
}

View file

@ -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"