Skip to content

Commit

Permalink
Merge pull request #101 from CrisisCleanup/error-message-navigation
Browse files Browse the repository at this point in the history
Error message navigation
  • Loading branch information
hueachilles authored Feb 6, 2024
2 parents 93b4adc + 728537a commit f1b31cb
Show file tree
Hide file tree
Showing 18 changed files with 203 additions and 121 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ plugins {

android {
defaultConfig {
val buildVersion = 186
val buildVersion = 187
applicationId = "com.crisiscleanup"
versionCode = buildVersion
versionName = "0.9.${buildVersion - 168}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.crisiscleanup.core.appnav.RouteConstant.casesGraphRoutePattern
import com.crisiscleanup.core.appnav.navigateToExistingCase
import com.crisiscleanup.core.data.model.ExistingWorksiteIdentifier
import com.crisiscleanup.core.model.data.EmptyIncident
import com.crisiscleanup.core.model.data.EmptyWorksite
Expand All @@ -21,7 +22,6 @@ import com.crisiscleanup.feature.caseeditor.navigation.existingCaseScreen
import com.crisiscleanup.feature.caseeditor.navigation.existingCaseTransferWorkTypesScreen
import com.crisiscleanup.feature.caseeditor.navigation.navigateToCaseAddFlag
import com.crisiscleanup.feature.caseeditor.navigation.navigateToCaseEditor
import com.crisiscleanup.feature.caseeditor.navigation.navigateToExistingCase
import com.crisiscleanup.feature.caseeditor.navigation.navigateToTransferWorkType
import com.crisiscleanup.feature.caseeditor.navigation.rerouteToCaseChange
import com.crisiscleanup.feature.cases.navigation.casesFilterScreen
Expand Down Expand Up @@ -52,7 +52,7 @@ import com.crisiscleanup.feature.userfeedback.navigation.userFeedbackScreen
fun CrisisCleanupNavHost(
navController: NavHostController,
onBack: () -> Unit,
openAuthentication: () -> Unit = {},
openAuthentication: () -> Unit,
modifier: Modifier = Modifier,
startDestination: String = casesGraphRoutePattern,
) {
Expand Down Expand Up @@ -80,14 +80,6 @@ fun CrisisCleanupNavHost(
}
}

val replaceRouteOpenCase = remember(navController) {
{ incidentId: Long, worksiteId: Long ->
navController.popBackStack()
viewCase(incidentId, worksiteId)
true
}
}

val replaceRouteViewCase = remember(navController) {
{ ids: ExistingWorksiteIdentifier -> navController.rerouteToCaseChange(ids) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,22 @@ import javax.inject.Singleton

private fun Json.parseNetworkErrors(response: String): List<NetworkCrisisCleanupApiError> {
var errors: List<NetworkCrisisCleanupApiError> = emptyList()
try {
val networkErrors = decodeFromString<NetworkErrors>(response)
errors = networkErrors.errors
} catch (e: Exception) {
// No errors or not formatted as expected
if (response.contains("errors")) {
try {
val networkErrors = decodeFromString<NetworkErrors>(response)
errors = networkErrors.errors
} catch (e: Exception) {
// No errors or not formatted as expected
}
}
return errors
}

private fun Json.parseNetworkErrors(responseBody: ResponseBody): List<NetworkCrisisCleanupApiError> {
private fun Json.parseNetworkErrors(responseBody: ResponseBody): Pair<String, List<NetworkCrisisCleanupApiError>> {
// TODO Large body can result in OOM. Parse in parts as necessary.
val errorBody = responseBody.string()
return parseNetworkErrors(errorBody)
val errors = parseNetworkErrors(errorBody)
return Pair(errorBody, errors)
}

private val Request.pathsForLog: String
Expand Down Expand Up @@ -204,12 +208,13 @@ class CrisisCleanupInterceptorProvider @Inject constructor(
getHeaderKey(request, RequestHeaderKey.ThrowClientError)?.let {
response.body?.let { responseBody ->
logger.logCapture("Code ${response.code} message ${response.message} paths ${request.pathsForLog}")
val errors = json.parseNetworkErrors(responseBody)
val (body, errors) = json.parseNetworkErrors(responseBody)
throw CrisisCleanupNetworkException(
request.url.toUrl().toString(),
response.code,
response.message,
errors,
body,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.crisiscleanup.core.appnav

import androidx.navigation.NavController
import com.crisiscleanup.core.appnav.RouteConstant.viewCaseRoute

const val incidentIdArg = "incidentId"
const val worksiteIdArg = "worksiteId"

fun NavController.navigateToExistingCase(incidentId: Long, worksiteId: Long) {
// Must match composable route signature
this.navigate("${viewCaseRoute}?$incidentIdArg=$incidentId&$worksiteIdArg=$worksiteId")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import com.crisiscleanup.core.common.log.Logger
import com.crisiscleanup.core.model.data.CodeInviteAccept
import com.crisiscleanup.core.model.data.IncidentOrganizationInviteInfo
import com.crisiscleanup.core.model.data.InvitationRequest
import com.crisiscleanup.core.model.data.InvitationRequestResult
import com.crisiscleanup.core.model.data.JoinOrgInvite
import com.crisiscleanup.core.model.data.JoinOrgResult
import com.crisiscleanup.core.model.data.OrgInviteResult
import com.crisiscleanup.core.model.data.OrgUserInviteInfo
import com.crisiscleanup.core.network.CrisisCleanupRegisterApi
import kotlinx.datetime.Instant
Expand All @@ -23,7 +25,7 @@ interface OrgVolunteerRepository {
suspend fun getOrganizationInvite(organizationId: Long, inviterUserId: Long): JoinOrgInvite
suspend fun acceptPersistentInvitation(invite: CodeInviteAccept): JoinOrgResult

suspend fun inviteToOrganization(emailAddress: String, organizationId: Long?): Boolean
suspend fun inviteToOrganization(emailAddress: String, organizationId: Long?): OrgInviteResult
suspend fun createOrganization(
referer: String,
invite: IncidentOrganizationInviteInfo,
Expand All @@ -37,11 +39,7 @@ class CrisisCleanupOrgVolunteerRepository @Inject constructor(
override suspend fun requestInvitation(invite: InvitationRequest): InvitationRequestResult? {
try {
// TODO Handle cases where an invite was already sent to the user from the org
val result = registerApi.registerOrgVolunteer(invite)
return InvitationRequestResult(
organizationName = result.requestedOrganization,
organizationRecipient = result.requestedTo,
)
return registerApi.registerOrgVolunteer(invite)
} catch (e: Exception) {
logger.logException(e)
}
Expand Down Expand Up @@ -107,13 +105,13 @@ class CrisisCleanupOrgVolunteerRepository @Inject constructor(
override suspend fun inviteToOrganization(
emailAddress: String,
organizationId: Long?,
): Boolean {
): OrgInviteResult {
try {
return registerApi.inviteToOrganization(emailAddress, organizationId)
} catch (e: Exception) {
logger.logException(e)
}
return false
return OrgInviteResult.Unknown
}

override suspend fun createOrganization(
Expand All @@ -128,8 +126,3 @@ class CrisisCleanupOrgVolunteerRepository @Inject constructor(
return false
}
}

data class InvitationRequestResult(
val organizationName: String,
val organizationRecipient: String,
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.crisiscleanup.core.database.dao

import androidx.room.*
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import com.crisiscleanup.core.database.model.PopulatedRecentWorksite
import com.crisiscleanup.core.database.model.RecentWorksiteEntity
import kotlinx.coroutines.flow.Flow
Expand All @@ -13,9 +16,10 @@ interface RecentWorksiteDao {
@Transaction
@Query(
"""
SELECT *
FROM recent_worksites
WHERE incident_id=:incidentId
SELECT r.*
FROM recent_worksites r
INNER JOIN worksites w ON r.id=w.id
WHERE r.incident_id=:incidentId AND w.incident_id=:incidentId
ORDER BY viewed_at DESC
LIMIT :limit
OFFSET :offset
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fun RegisterSuccessView(

Spacer(Modifier.weight(1f))

CrisisCleanupLogoRow(Modifier,true)
CrisisCleanupLogoRow(Modifier, true)

Spacer(Modifier.weight(1f))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ data class InvitationRequest(
val inviterEmailAddress: String,
)

data class InvitationRequestResult(
val organizationName: String,
val organizationRecipient: String,
val isNewAccountRequest: Boolean,
)

data class IncidentOrganizationInviteInfo(
val incidentId: Long,
val organizationName: String,
Expand All @@ -32,3 +38,9 @@ data class CodeInviteAccept(

val invitationCode: String,
)

enum class OrgInviteResult {
Invited,
Redundant,
Unknown,
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import com.crisiscleanup.core.common.event.UserPersistentInvite
import com.crisiscleanup.core.model.data.CodeInviteAccept
import com.crisiscleanup.core.model.data.IncidentOrganizationInviteInfo
import com.crisiscleanup.core.model.data.InvitationRequest
import com.crisiscleanup.core.model.data.InvitationRequestResult
import com.crisiscleanup.core.model.data.JoinOrgResult
import com.crisiscleanup.core.model.data.OrgInviteResult
import com.crisiscleanup.core.model.data.OrgUserInviteInfo
import com.crisiscleanup.core.network.model.NetworkAcceptedInvitationRequest
import com.crisiscleanup.core.network.model.NetworkPersistentInvitation

interface CrisisCleanupRegisterApi {
suspend fun registerOrgVolunteer(invite: InvitationRequest): NetworkAcceptedInvitationRequest
suspend fun registerOrgVolunteer(invite: InvitationRequest): InvitationRequestResult?

suspend fun getInvitationInfo(invite: UserPersistentInvite): OrgUserInviteInfo?

Expand All @@ -25,7 +26,7 @@ interface CrisisCleanupRegisterApi {

suspend fun acceptPersistentInvitation(invite: CodeInviteAccept): JoinOrgResult

suspend fun inviteToOrganization(emailAddress: String, organizationId: Long?): Boolean
suspend fun inviteToOrganization(emailAddress: String, organizationId: Long?): OrgInviteResult

suspend fun registerOrganization(
referer: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class CrisisCleanupNetworkException(
val statusCode: Int,
message: String,
val errors: List<NetworkCrisisCleanupApiError>,
val body: String = "",
) : IOException(message)

@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import com.crisiscleanup.core.model.data.CodeInviteAccept
import com.crisiscleanup.core.model.data.ExpiredNetworkOrgInvite
import com.crisiscleanup.core.model.data.IncidentOrganizationInviteInfo
import com.crisiscleanup.core.model.data.InvitationRequest
import com.crisiscleanup.core.model.data.InvitationRequestResult
import com.crisiscleanup.core.model.data.JoinOrgResult
import com.crisiscleanup.core.model.data.OrgInviteResult
import com.crisiscleanup.core.model.data.OrgUserInviteInfo
import com.crisiscleanup.core.network.CrisisCleanupRegisterApi
import com.crisiscleanup.core.network.model.CrisisCleanupNetworkException
import com.crisiscleanup.core.network.model.NetworkAcceptCodeInvite
import com.crisiscleanup.core.network.model.NetworkAcceptPersistentInvite
import com.crisiscleanup.core.network.model.NetworkAcceptedCodeInvitationRequest
Expand All @@ -26,6 +29,7 @@ import com.crisiscleanup.core.network.model.NetworkPersistentInvitation
import com.crisiscleanup.core.network.model.NetworkPersistentInvitationResult
import com.crisiscleanup.core.network.model.NetworkRegisterOrganizationResult
import com.crisiscleanup.core.network.model.NetworkUser
import com.crisiscleanup.core.network.model.condenseMessages
import com.crisiscleanup.core.network.model.profilePictureUrl
import retrofit2.Retrofit
import retrofit2.http.Body
Expand All @@ -38,6 +42,7 @@ import javax.inject.Inject
import javax.inject.Singleton

private interface RegisterApi {
@ThrowClientErrorHeader
@POST("invitation_requests")
suspend fun requestInvitation(
@Body invitationRequest: NetworkInvitationRequest,
Expand Down Expand Up @@ -79,13 +84,15 @@ private interface RegisterApi {
@Body org: NetworkCreateOrgInvitation,
): NetworkPersistentInvitationResult

@ThrowClientErrorHeader
@POST("persistent_invitations/accept")
suspend fun acceptPersistentInvitation(
@Body acceptInvite: NetworkAcceptPersistentInvite,
): NetworkAcceptedPersistentInvite

@TokenAuthenticationHeader
@WrapResponseHeader("invite")
@ThrowClientErrorHeader
@POST("invitations")
suspend fun inviteToOrganization(
@Body invite: NetworkOrganizationInvite,
Expand All @@ -105,7 +112,7 @@ class RegisterApiClient @Inject constructor(
) : CrisisCleanupRegisterApi {
private val networkApi = retrofit.create(RegisterApi::class.java)

override suspend fun registerOrgVolunteer(invite: InvitationRequest): NetworkAcceptedInvitationRequest {
override suspend fun registerOrgVolunteer(invite: InvitationRequest): InvitationRequestResult? {
val inviteRequest = NetworkInvitationRequest(
firstName = invite.firstName,
lastName = invite.lastName,
Expand All @@ -117,7 +124,21 @@ class RegisterApiClient @Inject constructor(
requestedTo = invite.inviterEmailAddress,
primaryLanguage = invite.languageId,
)
return networkApi.requestInvitation(inviteRequest)
try {
val result = networkApi.requestInvitation(inviteRequest)
return InvitationRequestResult(
organizationName = result.requestedOrganization,
organizationRecipient = result.requestedTo,
isNewAccountRequest = true,
)
} catch (e: CrisisCleanupNetworkException) {
if (e.errors.condenseMessages.contains("already have an account")) {
return InvitationRequestResult("", "", false)
}
}

// TODO Be explicit in result
return null
}

private suspend fun getUserDetails(userId: Long): UserDetails {
Expand Down Expand Up @@ -217,22 +238,43 @@ class RegisterApiClient @Inject constructor(
mobile = invite.mobile,
token = invite.invitationCode,
)
val response = networkApi.acceptPersistentInvitation(payload)
// TODO Parse response.detail even when response code is 400
return when (response.detail) {
"You have been added to the organization." -> JoinOrgResult.Success
"User already a member of this organization." -> JoinOrgResult.Redundant
else -> JoinOrgResult.Unknown
try {
val response = networkApi.acceptPersistentInvitation(payload)
return when (response.detail) {
"You have been added to the organization." -> JoinOrgResult.Success
"User already a member of this organization." -> JoinOrgResult.Redundant
else -> JoinOrgResult.Unknown
}
} catch (e: CrisisCleanupNetworkException) {
if (e.body.contains("User already a member of this organization.")) {
return JoinOrgResult.Redundant
}
}
return JoinOrgResult.Unknown
}

override suspend fun inviteToOrganization(
emailAddress: String,
organizationId: Long?,
): Boolean {
val invite =
networkApi.inviteToOrganization(NetworkOrganizationInvite(emailAddress, organizationId))
return invite.invite?.inviteeEmail == emailAddress
): OrgInviteResult {
try {
val invite =
networkApi.inviteToOrganization(
NetworkOrganizationInvite(
emailAddress,
organizationId,
),
)
if (invite.invite?.inviteeEmail == emailAddress) {
return OrgInviteResult.Invited
}
} catch (e: CrisisCleanupNetworkException) {
if (e.errors.condenseMessages.contains("is already a part of this organization")) {
return OrgInviteResult.Redundant
}
}

return OrgInviteResult.Unknown
}

override suspend fun registerOrganization(
Expand Down
Loading

0 comments on commit f1b31cb

Please sign in to comment.