Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2f6d1d0
feat: add PartnerClaim type to GraphQL schema
hugokallstrom Apr 23, 2026
c46ab04
feat: add PartnerClaimFragment GraphQL fragment
hugokallstrom Apr 23, 2026
07a2a6e
feat: add fromPartnerClaim factory methods for claim card UI state
hugokallstrom Apr 23, 2026
a2a1ff2
feat: merge partner claims into home screen claims pager
hugokallstrom Apr 23, 2026
72b6ce9
feat: dispatch partner claim navigation from home screen
hugokallstrom Apr 23, 2026
08d0b42
feat: merge partner claims into claim history
hugokallstrom Apr 23, 2026
f5210ee
refactor: extract shared claim detail composables to ui-claim-status
hugokallstrom Apr 23, 2026
7740623
feat: add partner claim details module with simplified details screen
hugokallstrom Apr 23, 2026
d7cce70
feat: wire partner claim details into app navigation and DI
hugokallstrom Apr 23, 2026
ad5512e
fix: right-align arrow icon in partner claim email row
hugokallstrom Apr 23, 2026
c43dae0
fix: match email row styling to chat row pattern in claim details
hugokallstrom Apr 23, 2026
ad3d88c
fix: simplify email row to plain clickable card
hugokallstrom Apr 23, 2026
de0f652
fix: remove handler email from partner claim details screen
hugokallstrom Apr 23, 2026
7b9621a
chore: update GraphQL schema from introspection
hugokallstrom Apr 23, 2026
221c87c
chore: run ktlint formatting
hugokallstrom Apr 23, 2026
94dd5cb
fix: handle empty claims list in home status cards and add lint baseline
hugokallstrom Apr 23, 2026
a69ea1e
docs: add design spec for unifying partner claim into regular claim d…
hugokallstrom Apr 24, 2026
4cbe579
docs: add implementation plan for unifying partner claim detail
hugokallstrom Apr 24, 2026
16e69fb
feat: add isPartnerClaim flag to claim detail destination and wire th…
hugokallstrom Apr 24, 2026
9759d7d
feat: add partner claim query support to GetClaimDetailUiStateUseCase
hugokallstrom Apr 24, 2026
4e19eea
fix: hide uploaded files header when no submitted content or files
hugokallstrom Apr 24, 2026
2695345
refactor: unify claim detail navigation — remove partner-specific cal…
hugokallstrom Apr 24, 2026
75a1517
refactor: delete feature-partner-claim-details module and all references
hugokallstrom Apr 24, 2026
dffcca6
chore: remove accidentally staged worktree
hugokallstrom Apr 24, 2026
6bc0a31
chore: run ktlint formatting
hugokallstrom Apr 24, 2026
042cabe
fix: use kotlin.time.Clock instead of kotlinx.datetime.Clock
hugokallstrom Apr 24, 2026
474926b
fix: show submitted date instead of exposure name on partner claim cards
hugokallstrom Apr 27, 2026
ed5daf4
chore: remove design spec and implementation plan docs
hugokallstrom Apr 27, 2026
2d90eb6
refactor: move shared claim detail composables back to feature-claim-…
hugokallstrom Apr 27, 2026
3089a81
chore: restore original function ordering in ClaimDetailsDestination …
hugokallstrom Apr 27, 2026
0847a4b
fix: address review on partner claim flow + iOS parity
hugokallstrom May 7, 2026
cea78ef
chore: download strings
hugokallstrom May 7, 2026
f486b9e
refactor: simplify partner-claim fallback in GetClaimDetailUiStateUse…
hugokallstrom May 7, 2026
a44fe35
chore: add isPartnerClaim flag to ClaimStatusCardUiState
hugokallstrom May 7, 2026
318ca9a
Revert "chore: add isPartnerClaim flag to ClaimStatusCardUiState"
hugokallstrom May 7, 2026
28892c2
Merge remote-tracking branch 'origin/develop' into feat/show-car-claims
hugokallstrom May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
fragment PartnerClaimFragment on PartnerClaim {
id
externalId
exposureDisplayName
status
submittedAt
payoutAmount {
...MoneyFragment
}
associatedTypeOfContract
claimType
handlerEmail
displayItems {
displayTitle
displayValue
}
productVariant {
typeOfContract
displayName
documents {
type
url
}
}
}
1 change: 0 additions & 1 deletion app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ dependencies {
implementation(projects.featureClaimChat)
implementation(projects.featureClaimDetails)
implementation(projects.featureClaimHistory)

implementation(projects.featureConnectPaymentTrustly)
implementation(projects.featureCrossSellSheet)
implementation(projects.featureDeleteAccount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ internal fun HedvigNavHost(
navigateToChipId = {
navController.navigate(ChipIdGraphDestination())
},
languageService = languageService
languageService = languageService,
)
cbmChatGraph(
hedvigDeepLinkContainer = hedvigDeepLinkContainer,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
query PartnerClaimDetail($claimId: ID!) {
partnerClaim(id: $claimId) {
...PartnerClaimFragment
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.hedvig.android.data.display.items.DisplayItem
import com.hedvig.android.feature.claim.details.ui.ClaimDetailUiState
import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState
import com.hedvig.audio.player.data.SignedAudioUrl
import kotlin.time.Clock
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
Expand All @@ -22,9 +23,12 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toLocalDateTime
import octopus.ClaimQuery
import octopus.PartnerClaimDetailQuery
import octopus.fragment.ClaimFragment
import octopus.fragment.PartnerClaimFragment
import octopus.type.ClaimOutcome
import octopus.type.ClaimStatus
import octopus.type.InsuranceDocumentType
Expand All @@ -33,14 +37,25 @@ internal class GetClaimDetailUiStateUseCase(
private val apolloClient: ApolloClient,
private val crossSellAfterClaimClosedRepository: CrossSellAfterClaimClosedRepository,
) {
fun invoke(claimId: String): Flow<Either<Error, ClaimDetailUiState.Content>> {
return flow {
while (currentCoroutineContext().isActive) {
val queryFlow = queryFlow(claimId)
emitAll(queryFlow)
delay(POLL_INTERVAL)
fun invoke(claimId: String): Flow<Either<Error, ClaimDetailUiState.Content>> = flow {
// First iteration: try the regular endpoint. A NoClaimFound here means this
// is a partner claim — fall back to the partner endpoint and remember that
// choice for the polling loop below.
var fellBackToPartner = false
queryFlow(claimId).collect { result ->
if (result == Either.Left(Error.NoClaimFound)) {
fellBackToPartner = true
} else {
emit(result)
}
}
if (fellBackToPartner) emitAll(partnerQueryFlow(claimId))

val pollEndpoint = if (fellBackToPartner) ::partnerQueryFlow else ::queryFlow
while (currentCoroutineContext().isActive) {
delay(POLL_INTERVAL)
emitAll(pollEndpoint(claimId))
}
}

private fun queryFlow(claimId: String): Flow<Either<Error, ClaimDetailUiState.Content>> {
Expand All @@ -60,6 +75,65 @@ internal class GetClaimDetailUiStateUseCase(
}
}

private fun partnerQueryFlow(claimId: String): Flow<Either<Error, ClaimDetailUiState.Content>> {
return apolloClient
.query(PartnerClaimDetailQuery(claimId))
.fetchPolicy(FetchPolicy.NetworkOnly)
.safeFlow { Error.NetworkError }
.map { response ->
either {
val claim = response.bind().partnerClaim
ensureNotNull(claim) { Error.NoClaimFound }
fromPartnerClaim(claim)
}
}
}

private fun fromPartnerClaim(claim: PartnerClaimFragment): ClaimDetailUiState.Content {
val termsConditionsUrl = claim.productVariant?.documents
?.firstOrNull { it.type == InsuranceDocumentType.TERMS_AND_CONDITIONS }?.url
val submittedAt = claim.submittedAt
?.atStartOfDayIn(TimeZone.UTC)
?.toLocalDateTime(TimeZone.UTC)
?: Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())

return ClaimDetailUiState.Content(
claimId = claim.id,
conversationId = null,
hasUnreadMessages = false,
submittedContent = null,
files = emptyList(),
claimStatusCardUiState = ClaimStatusCardUiState.fromPartnerClaim(claim),
claimStatus = when (claim.status) {
ClaimStatus.CREATED -> ClaimDetailUiState.Content.ClaimStatus.CREATED
ClaimStatus.IN_PROGRESS -> ClaimDetailUiState.Content.ClaimStatus.IN_PROGRESS
ClaimStatus.CLOSED -> ClaimDetailUiState.Content.ClaimStatus.CLOSED
ClaimStatus.REOPENED -> ClaimDetailUiState.Content.ClaimStatus.REOPENED
ClaimStatus.UNKNOWN__, null -> ClaimDetailUiState.Content.ClaimStatus.UNKNOWN
},
claimOutcome = ClaimDetailUiState.Content.ClaimOutcome.UNKNOWN,
Copy link
Copy Markdown
Contributor

@panasetskaya panasetskaya May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember there being some problems with having the outcome here from the BE, right? So we can't really have status "Paid" and payout can only be mentioned in the details?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no "10000 kr", "Paid" pill on the card, I mean

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, we dont get outcome from EIR.

uploadUri = "",
isUploadingFile = false,
uploadError = null,
claimType = claim.claimType,
insuranceDisplayName = claim.exposureDisplayName ?: claim.productVariant?.displayName,
submittedAt = submittedAt,
termsConditionsUrl = termsConditionsUrl,
savedFileUri = null,
downloadError = null,
isLoadingPdf = null,
appealInstructionsUrl = null,
isUploadingFilesEnabled = false,
infoText = null,
displayItems = claim.displayItems.map {
DisplayItem.fromStrings(it.displayTitle, it.displayValue)
},
externalId = claim.externalId,
handlerEmail = claim.handlerEmail,
isPartnerClaim = true,
)
}

private fun ClaimDetailUiState.Content.Companion.fromClaim(
claim: ClaimFragment,
conversationId: String?,
Expand Down Expand Up @@ -146,6 +220,9 @@ internal class GetClaimDetailUiStateUseCase(
displayItems = claim.displayItems.map {
DisplayItem.fromStrings(it.displayTitle, it.displayValue)
},
externalId = null,
handlerEmail = null,
isPartnerClaim = false,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.hedvig.android.feature.claim.details.ui

import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
Expand Down Expand Up @@ -34,10 +38,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.content.getSystemService
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.ImageLoader
import com.eygraber.uri.toAndroidUri
Expand Down Expand Up @@ -110,6 +116,8 @@ import hedvig.resources.claim_outcome_unresponsive_support_text
import hedvig.resources.claim_status_appeal_instruction_link_text
import hedvig.resources.claim_status_being_handled_reopened_support_text
import hedvig.resources.claim_status_being_handled_support_text
import hedvig.resources.claim_status_claim_details_external_id
import hedvig.resources.claim_status_claim_details_handler_email
import hedvig.resources.claim_status_claim_details_info_text
import hedvig.resources.claim_status_claim_details_title
import hedvig.resources.claim_status_detail_add_files
Expand All @@ -120,10 +128,12 @@ import hedvig.resources.claim_status_detail_uploaded_files_info_title
import hedvig.resources.claim_status_not_compensated_support_text
import hedvig.resources.claim_status_not_covered_support_text
import hedvig.resources.claim_status_paid_support_text_short
import hedvig.resources.claim_status_partner_support_text
import hedvig.resources.claim_status_submitted_support_text
import hedvig.resources.claim_status_uploaded_files_upload_text
import hedvig.resources.general_close_button
import hedvig.resources.general_error
import hedvig.resources.referrals_active__toast_text
import hedvig.resources.something_went_wrong
import hedvig.resources.travel_certificate_downloading_error
import java.io.File
Expand Down Expand Up @@ -380,12 +390,11 @@ private fun NonDynamicGrid(
}

@Composable
internal fun ExplanationBottomSheet(sheetState: HedvigBottomSheetState<Unit>) {
private fun ExplanationBottomSheet(sheetState: HedvigBottomSheetState<Unit>) {
HedvigBottomSheet(sheetState) { _ ->
HedvigText(
text = stringResource(Res.string.claim_status_claim_details_info_text),
modifier = Modifier
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(32.dp))
HedvigTextButton(
Expand Down Expand Up @@ -423,7 +432,7 @@ private fun BeforeGridContent(
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
if (!uiState.claimIsInUndeterminedState) {
HedvigText(
text = statusParagraphText(uiState.claimStatus, uiState.claimOutcome),
text = statusParagraphText(uiState.claimStatus, uiState.claimOutcome, uiState.isPartnerClaim),
style = HedvigTheme.typography.bodySmall,
)
}
Expand Down Expand Up @@ -504,29 +513,38 @@ private fun BeforeGridContent(
.fillMaxWidth()
.padding(horizontal = 2.dp),
)
Spacer(Modifier.height(24.dp))
HedvigText(
stringResource(Res.string.claim_status_detail_uploaded_files_info_title),
Modifier.padding(horizontal = 2.dp),
PartnerInfoRows(
externalId = uiState.externalId,
handlerEmail = uiState.handlerEmail,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp),
)
Spacer(Modifier.height(8.dp))
when (uiState.submittedContent) {
is ClaimDetailUiState.Content.SubmittedContent.Audio -> {
ClaimDetailHedvigAudioPlayerItem(uiState.submittedContent.signedAudioURL)
}
if (uiState.submittedContent != null || uiState.files.isNotEmpty()) {
Spacer(Modifier.height(24.dp))
HedvigText(
stringResource(Res.string.claim_status_detail_uploaded_files_info_title),
Modifier.padding(horizontal = 2.dp),
)
Spacer(Modifier.height(8.dp))
when (uiState.submittedContent) {
is ClaimDetailUiState.Content.SubmittedContent.Audio -> {
ClaimDetailHedvigAudioPlayerItem(uiState.submittedContent.signedAudioURL)
}

is ClaimDetailUiState.Content.SubmittedContent.FreeText -> {
HedvigCard(Modifier.fillMaxWidth()) {
HedvigText(
uiState.submittedContent.text,
Modifier.padding(16.dp),
)
is ClaimDetailUiState.Content.SubmittedContent.FreeText -> {
HedvigCard(Modifier.fillMaxWidth()) {
HedvigText(
uiState.submittedContent.text,
Modifier.padding(16.dp),
)
}
}
}

else -> {}
else -> {}
}
Spacer(Modifier.height(8.dp))
}
Spacer(Modifier.height(8.dp))
}

@Composable
Expand Down Expand Up @@ -701,6 +719,18 @@ private fun DocumentCard(title: String) {
private fun statusParagraphText(
claimStatus: ClaimDetailUiState.Content.ClaimStatus,
claimOutcome: ClaimDetailUiState.Content.ClaimOutcome,
isPartnerClaim: Boolean,
): String {
if (isPartnerClaim && claimStatus != ClaimDetailUiState.Content.ClaimStatus.CLOSED) {
return stringResource(Res.string.claim_status_partner_support_text)
}
return regularStatusParagraphText(claimStatus, claimOutcome)
}

@Composable
private fun regularStatusParagraphText(
claimStatus: ClaimDetailUiState.Content.ClaimStatus,
claimOutcome: ClaimDetailUiState.Content.ClaimOutcome,
): String = when (claimStatus) {
ClaimDetailUiState.Content.ClaimStatus.CREATED -> {
stringResource(Res.string.claim_status_submitted_support_text)
Expand Down Expand Up @@ -755,6 +785,53 @@ private fun ClaimDetailHedvigAudioPlayerItem(signedAudioUrl: SignedAudioUrl, mod
}
}

@Composable
private fun PartnerInfoRows(externalId: String?, handlerEmail: String?, modifier: Modifier = Modifier) {
if (externalId.isNullOrBlank() && handlerEmail.isNullOrBlank()) return
val context = LocalContext.current
val copiedToast = stringResource(Res.string.referrals_active__toast_text)
CompositionLocalProvider(LocalContentColor provides HedvigTheme.colorScheme.textSecondary) {
Column(modifier) {
if (!externalId.isNullOrBlank()) {
CopyableRow(
title = stringResource(Res.string.claim_status_claim_details_external_id),
value = externalId,
onCopy = { context.copyToClipboardAndShowToast(externalId, copiedToast) },
)
}
if (!handlerEmail.isNullOrBlank()) {
CopyableRow(
title = stringResource(Res.string.claim_status_claim_details_handler_email),
value = handlerEmail,
onCopy = { context.copyToClipboardAndShowToast(handlerEmail, copiedToast) },
)
}
}
}
}

@Composable
private fun CopyableRow(title: String, value: String, onCopy: () -> Unit) {
HorizontalItemsWithMaximumSpaceTaken(
spaceBetween = 8.dp,
modifier = Modifier.clickable(onClick = onCopy),
startSlot = {
HedvigText(text = title)
},
endSlot = {
HedvigText(
text = value,
textAlign = TextAlign.End,
)
},
)
}

private fun Context.copyToClipboardAndShowToast(text: String, toast: String) {
getSystemService<ClipboardManager>()?.setPrimaryClip(ClipData.newPlainText(null, text))
Toast.makeText(this, toast, Toast.LENGTH_SHORT).show()
}

@Composable
private fun DisplayItemsSection(displayItems: List<DisplayItem>, modifier: Modifier = Modifier) {
CompositionLocalProvider(LocalContentColor provides HedvigTheme.colorScheme.textSecondary) {
Expand Down Expand Up @@ -888,6 +965,9 @@ private fun PreviewClaimDetailScreen(
DisplayItem("Type", Text("Respiratory disorder")),
DisplayItem("Submitted", Text("2025-02-03")),
),
externalId = "EIR-2026-000123",
handlerEmail = "claims@eir.se",
isPartnerClaim = true,
),
openUrl = {},
imageLoader = rememberPreviewImageLoader(),
Expand Down
Loading
Loading