From 4093616f7e3a212498c2a8999021a18d51c87f1c Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 11:39:11 +0200 Subject: [PATCH 01/28] entrypoints with resumable claim --- .../app/navigation/HedvigEntryProvider.kt | 8 +++- .../system/hedvig/StartClaimBottomSheet.kt | 40 ++++++++++++++++++- .../feature/chat/inbox/InboxDestination.kt | 6 ++- .../claim/chat/navigation/ClaimChatEntries.kt | 1 + .../src/main/graphql/QueryHome.graphql | 3 ++ .../home/home/data/GetHomeDataUseCase.kt | 2 + .../home/home/data/GetHomeDataUseCaseDemo.kt | 1 + .../home/home/navigation/HomeEntries.kt | 4 +- .../feature/home/home/ui/HomeDestination.kt | 22 +++++++--- .../feature/home/home/ui/HomePresenter.kt | 5 +++ 10 files changed, 79 insertions(+), 13 deletions(-) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt index 36905da0e5..7b6f04cc6e 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt @@ -230,8 +230,12 @@ private fun EntryProviderScope.addHomeEntries( backstack.add(CoInsuredAddInfoKey(contractId, type)) }, navigateToHelpCenter = { backstack.add(HelpCenterKey) }, - navigateToClaimChat = { - backstack.add(ClaimChatKey(messageId = null, isDevelopmentFlow = false)) + navigateToClaimChat = { resumableClaimId -> + backstack.add(ClaimChatKey( + messageId = null, + isDevelopmentFlow = false, + resumableClaimId = resumableClaimId, + )) }, navigateToChipIdScreen = { backstack.add(ChipIdKey()) }, openAppSettings = externalNavigator::openAppSettings, diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt index 558f31da43..6b168aeef1 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt @@ -23,8 +23,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed +import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.icon.Checkmark import com.hedvig.android.design.system.hedvig.icon.HedvigIcons @@ -40,10 +42,12 @@ import hedvig.resources.general_cancel_button import hedvig.resources.general_continue_button import org.jetbrains.compose.resources.stringResource +data class StartClaimSheetData(val resumableClaimId: String?) @Composable fun StartClaimBottomSheet( - state: HedvigBottomSheetState, + state: HedvigBottomSheetState, navigateToClaimChat: () -> Unit, + navigateToOldClaim: () -> Unit, ) { HedvigBottomSheet( hedvigBottomSheetState = state, @@ -57,6 +61,12 @@ fun StartClaimBottomSheet( navigateToClaimChat() } }, + navigateToOldClaim = { + state.dismiss { + navigateToOldClaim() + } + }, + resumableClaimId = state.data?.resumableClaimId ) }, ) @@ -82,6 +92,8 @@ fun StartClaimPledgeScreen( }, navigateToClaimChat = navigateToClaimChat, dismiss = navigateUp, + resumableClaimId = null, + navigateToOldClaim = {} ) Spacer(Modifier.height(8.dp)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) @@ -92,6 +104,8 @@ fun StartClaimPledgeScreen( private fun StartClaimBottomSheetContent( dismiss: () -> Unit, navigateToClaimChat: () -> Unit, + navigateToOldClaim: () -> Unit, + resumableClaimId: String? ) { var isChecked by remember { mutableStateOf(false) } Column { @@ -113,6 +127,8 @@ private fun StartClaimBottomSheetContent( }, navigateToClaimChat = navigateToClaimChat, dismiss = dismiss, + navigateToOldClaim = navigateToOldClaim, + resumableClaimId = resumableClaimId ) Spacer(Modifier.height(8.dp)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) @@ -125,6 +141,8 @@ private fun StartClaimBottomContent( onCheckedChange: () -> Unit, navigateToClaimChat: () -> Unit, dismiss: () -> Unit, + navigateToOldClaim: () -> Unit, + resumableClaimId: String? ) { Column { ImportantInfoCheckBox( @@ -141,6 +159,18 @@ private fun StartClaimBottomContent( modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) + if (resumableClaimId!=null) { + HedvigButton( + buttonStyle = ButtonDefaults.ButtonStyle.PrimaryAlt, + text = "Continue with the draft claim", + enabled = true, + onClick = dropUnlessResumed { + navigateToOldClaim() + }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } HedvigButton( text = stringResource(Res.string.general_cancel_button), enabled = true, @@ -216,12 +246,18 @@ private fun ImportantInfoCheckBox(isChecked: Boolean, onCheckedChange: () -> Uni @HedvigPreview @Composable -private fun PreviewStartClaimBottomSheetContent() { +private fun PreviewStartClaimBottomSheetContent( + @PreviewParameter( + BooleanCollectionPreviewParameterProvider::class, + ) hasResumableClaim: Boolean, +) { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { StartClaimBottomSheetContent( {}, navigateToClaimChat = {}, + navigateToOldClaim = {}, + resumableClaimId = if (hasResumableClaim) "" else null ) } } diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt index 390005cba1..59d8410e99 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt @@ -61,6 +61,7 @@ import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightS import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken import com.hedvig.android.design.system.hedvig.Icon import com.hedvig.android.design.system.hedvig.StartClaimBottomSheet +import com.hedvig.android.design.system.hedvig.StartClaimSheetData import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TopAppBar import com.hedvig.android.design.system.hedvig.TopAppBarActionType @@ -131,7 +132,7 @@ private fun InboxScreen( navigateToClaimChat: () -> Unit, ) { val newChatSelectBottomSheetState = rememberHedvigBottomSheetState() - val startClaimBottomSheetState = rememberHedvigBottomSheetState() + val startClaimBottomSheetState = rememberHedvigBottomSheetState() HedvigBottomSheet( newChatSelectBottomSheetState, content = { @@ -142,7 +143,7 @@ private fun InboxScreen( }, onStartNewClaim = { newChatSelectBottomSheetState.dismiss() - startClaimBottomSheetState.show(Unit) + startClaimBottomSheetState.show(StartClaimSheetData(null)) }, dismiss = { newChatSelectBottomSheetState.dismiss() @@ -156,6 +157,7 @@ private fun InboxScreen( startClaimBottomSheetState.dismiss() navigateToClaimChat() }, + navigateToOldClaim = {} ) Surface( color = HedvigTheme.colorScheme.backgroundPrimary, diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt index 93494fd1bc..f0e6c7308b 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt @@ -21,6 +21,7 @@ import kotlinx.serialization.Serializable data class ClaimChatKey( val isDevelopmentFlow: Boolean = false, val messageId: String? = null, + val resumableClaimId: String? = null, ) : HedvigNavKey @Serializable diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index c333001488..c971555513 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -1,5 +1,8 @@ query Home($claimsHistoryFlag: Boolean!) { currentMember { + resumableClaimIntent { + id + } claims@skip(if: $claimsHistoryFlag) { ...ClaimFragment } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index c4624d0ea3..86a890c4c1 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -172,6 +172,7 @@ internal class GetHomeDataUseCaseImpl( crossSells = crossSells, travelBannerInfo = travelBannerInfo?.firstOrNull(), showChatIcon = showChatIcon, + resumableClaimId = homeQueryData.currentMember.resumableClaimIntent?.id ) }.onLeft { error: ApolloOperationError -> logcat(operationError = error) { "GetHomeDataUseCase failed with $error" } @@ -280,6 +281,7 @@ data class HomeData( val firstVetSections: List, val crossSells: CrossSellSheetData, val travelBannerInfo: AddonBannerInfo?, + val resumableClaimId: String? ) { @Immutable data class ClaimStatusCardsData( diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index 4ee160b208..fc50dfc2ea 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -56,6 +56,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ), travelBannerInfo = null, showChatIcon = false, + resumableClaimId = null ).right(), ) } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index fbac21457e..885cc4c4f9 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -25,7 +25,7 @@ fun EntryProviderScope.homeEntries( navigateToContactInfo: () -> Unit, navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToHelpCenter: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (String?) -> Unit, navigateToChipIdScreen: () -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, @@ -38,7 +38,7 @@ fun EntryProviderScope.homeEntries( viewModel = viewModel, onNavigateToInbox = dropUnlessResumed { onNavigateToInbox() }, onNavigateToNewConversation = dropUnlessResumed { onNavigateToNewConversation() }, - navigateToClaimChat = dropUnlessResumed { navigateToClaimChat() }, + navigateToClaimChat = navigateToClaimChat, onClaimDetailCardClicked = dropUnlessResumed { claimId: String -> navigateToClaimDetails(claimId) }, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index d0c15818c5..9ef4a72fa3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -86,6 +86,7 @@ import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.NotificationDefaults import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.design.system.hedvig.StartClaimBottomSheet +import com.hedvig.android.design.system.hedvig.StartClaimSheetData import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TooltipDefaults import com.hedvig.android.design.system.hedvig.TooltipDefaults.BeakDirection.TopEnd @@ -155,7 +156,7 @@ internal fun HomeDestination( viewModel: HomeViewModel, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (String?) -> Unit, onClaimDetailCardClicked: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, @@ -205,7 +206,7 @@ private fun HomeScreen( reload: () -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (String?) -> Unit, onClaimDetailCardClicked: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, @@ -237,10 +238,17 @@ private fun HomeScreen( onCrossSellClick = openCrossSellUrl, imageLoader = imageLoader, ) - val startClaimBottomSheetState = rememberHedvigBottomSheetState() + + val resumableClaimId = (uiState as? Success)?.resumableClaimId + val startClaimBottomSheetState = rememberHedvigBottomSheetState() StartClaimBottomSheet( state = startClaimBottomSheetState, - navigateToClaimChat = navigateToClaimChat, + navigateToOldClaim = { + navigateToClaimChat(resumableClaimId) + }, + navigateToClaimChat = { + navigateToClaimChat(null) + }, ) Box(Modifier.fillMaxSize()) { val toolbarHeight = 64.dp @@ -277,7 +285,9 @@ private fun HomeScreen( navigateToConnectPayment = navigateToConnectPayment, navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, - openClaimFlowSheet = startClaimBottomSheetState::show, + openClaimFlowSheet = { + startClaimBottomSheetState.show(StartClaimSheetData(resumableClaimId)) + }, openAppSettings = openAppSettings, openUrl = openUrl, navigateToMissingInfo = navigateToMissingInfo, @@ -796,6 +806,7 @@ private fun PreviewHomeScreen( flowType = FlowType.APP_TRAVEL_PLUS_SELL_OR_UPGRADE, ), isProduction = true, + resumableClaimId = null ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, @@ -881,6 +892,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( chatAction = ChatAction, addonBannerInfo = null, isProduction = true, + resumableClaimId = null ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index fafa5cb3bc..3569e4b513 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -138,6 +138,7 @@ internal class HomePresenter( crossSellsAction = successData.crossSellsAction, addonBannerInfo = successData.addonBannerInfo, isProduction = isProduction, + resumableClaimId = successData.resumableClaimId ) } } @@ -177,6 +178,7 @@ internal sealed interface HomeUiState { val isProduction: Boolean, override val isHelpCenterEnabled: Boolean, override val hasUnseenChatMessages: Boolean, + val resumableClaimId: String? ) : HomeUiState data class Error(val message: String?) : HomeUiState @@ -195,6 +197,7 @@ private data class SuccessData( val crossSellsAction: HomeTopBarAction.CrossSellsAction?, val hasUnseenChatMessages: Boolean, val addonBannerInfo: AddonBannerInfo?, + val resumableClaimId: String? ) { companion object { fun fromLastState(lastState: HomeUiState): SuccessData? { @@ -210,6 +213,7 @@ private data class SuccessData( hasUnseenChatMessages = lastState.hasUnseenChatMessages, addonBannerInfo = lastState.addonBannerInfo, chatAction = lastState.chatAction, + resumableClaimId = lastState.resumableClaimId ) } @@ -257,6 +261,7 @@ private data class SuccessData( hasUnseenChatMessages = homeData.hasUnseenChatMessages, addonBannerInfo = homeData.travelBannerInfo, chatAction = if (homeData.showChatIcon) HomeTopBarAction.ChatAction else null, + resumableClaimId = homeData.resumableClaimId ) } } From 8817524e0ab6751c95710891378b32e0dac2592b Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 16:17:08 +0200 Subject: [PATCH 02/28] mapping of previous steps --- .../claim/chat/data/AudioRecordingManager.kt | 4 +- .../claim/chat/navigation/ClaimChatEntries.kt | 1 + .../graphql/FragmentClaimIntent.graphql | 19 ++++ .../graphql/ResumeClaimQuery.graphql | 7 ++ .../feature/claim/chat/ClaimChatViewModel.kt | 87 ++++++++++++++----- .../feature/claim/chat/data/ClaimIntent.kt | 14 ++- .../feature/claim/chat/data/ClaimIntentExt.kt | 37 +++++++- .../claim/chat/data/ResumeClaimUseCase.kt | 39 +++++++++ .../claim/chat/ui/ClaimChatDestination.kt | 3 +- .../claim/chat/ui/step/UploadFilesStep.kt | 1 + .../AudioRecordingStepSections.kt | 28 ++++-- 11 files changed, 202 insertions(+), 38 deletions(-) create mode 100644 app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql create mode 100644 app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/data/AudioRecordingManager.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/data/AudioRecordingManager.kt index f1e086ab48..63eead9f66 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/data/AudioRecordingManager.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/data/AudioRecordingManager.kt @@ -86,7 +86,7 @@ internal class AndroidAudioRecordingManager( if (!file.exists()) { onStateUpdate( AudioRecordingStepState.AudioRecording.Playback( - filePath = filePath, + audioPath = AudioPath.FilePath(filePath), isPlaying = false, isPrepared = false, hasError = true, @@ -100,7 +100,7 @@ internal class AndroidAudioRecordingManager( setOnPreparedListener { onStateUpdate( AudioRecordingStepState.AudioRecording.Playback( - filePath = filePath, + audioPath = AudioPath.FilePath(filePath), isPlaying = false, isPrepared = true, hasError = false, diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt index f0e6c7308b..7ca634c95f 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt @@ -59,6 +59,7 @@ fun EntryProviderScope.claimChatEntries( ) { entry { key -> ClaimChatDestination( + resumableClaimId = key.resumableClaimId, isDevelopmentFlow = key.isDevelopmentFlow, shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale, openAppSettings = openAppSettings, diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql index 14e8d0d94e..bc37b4b02e 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql @@ -14,6 +14,16 @@ fragment ClaimIntentFragment on ClaimIntent { id submittedAt } + createdAt + previousSteps { + id + text + hint + content { + ...ClaimIntentStepContentFragment + } + isRegrettable + } } fragment ClaimIntentStepContentFragment on ClaimIntentStepContent { @@ -44,6 +54,7 @@ fragment FormFragment on ClaimIntentStepContentForm { value } defaultValues + currentValues minValue maxValue searchData { @@ -63,6 +74,7 @@ fragment ContentSelectFragment on ClaimIntentStepContentSelect { isSkippable style defaultSelectedId + currentSelectedId } fragment TaskFragment on ClaimIntentStepContentTask { @@ -75,11 +87,18 @@ fragment AudioRecordingFragment on ClaimIntentStepContentAudioRecording { isSkippable freeTextMinLength freeTextMaxLength + currentAudioUrl + currentFreeText } fragment FileUploadFragment on ClaimIntentStepContentFileUpload { uploadUri isSkippable + currentFiles { + url + contentType + fileName + } } fragment SummaryFragment on ClaimIntentStepContentSummary { diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql new file mode 100644 index 0000000000..21231fdcd1 --- /dev/null +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql @@ -0,0 +1,7 @@ +query ResumeClaim { + currentMember { + resumableClaimIntent { + ...ClaimIntentFragment + } + } +} diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index 97e4b68f97..fb7d9210a6 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -34,6 +34,7 @@ import com.hedvig.feature.claim.chat.data.FormSubmissionData.FieldToSubmit import com.hedvig.feature.claim.chat.data.FreeTextErrorType.TooShort import com.hedvig.feature.claim.chat.data.GetClaimIntentUseCase import com.hedvig.feature.claim.chat.data.RegretStepUseCase +import com.hedvig.feature.claim.chat.data.ResumeClaimUseCase import com.hedvig.feature.claim.chat.data.SkipStepUseCase import com.hedvig.feature.claim.chat.data.StartClaimIntentUseCase import com.hedvig.feature.claim.chat.data.StepContent @@ -164,6 +165,7 @@ internal sealed interface ClaimChatUiState { @HedvigViewModel(ActivityRetainedScope::class) internal class ClaimChatViewModel( @Assisted developmentFlow: Boolean, + @Assisted resumableClaimId: String?, startClaimIntentUseCase: StartClaimIntentUseCase, getClaimIntentUseCase: GetClaimIntentUseCase, submitTaskUseCase: SubmitTaskUseCase, @@ -177,6 +179,7 @@ internal class ClaimChatViewModel( regretStepUseCase: RegretStepUseCase, formFieldSearchUseCase: FormFieldSearchUseCase, fileService: FileService, + resumeClaimUseCase: ResumeClaimUseCase ) : MoleculeViewModel( ClaimChatUiState.Initializing, ClaimChatPresenter( @@ -194,6 +197,8 @@ internal class ClaimChatViewModel( fileService, regretStepUseCase, formFieldSearchUseCase, + resumableClaimId, + resumeClaimUseCase ), ) { override fun onCleared() { @@ -217,6 +222,8 @@ internal class ClaimChatPresenter( private val fileService: FileService, private val regretStepUseCase: RegretStepUseCase, private val formFieldSearchUseCase: FormFieldSearchUseCase, + private val resumableClaimId: String?, + private val resumeClaimUseCase: ResumeClaimUseCase ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: ClaimChatUiState): ClaimChatUiState { @@ -254,34 +261,70 @@ internal class ClaimChatPresenter( var searchQuery by remember { mutableStateOf(null) } - if (initializing) { + if (initializing) { //TODO LaunchedEffect(Unit) { - startClaimIntentUseCase - .invoke(developmentFlow) - .fold( - ifLeft = { - initializing = false - failedToStart = true - }, - ifRight = { claimIntent -> - Snapshot.withMutableSnapshot { + val isResumingClaim = resumableClaimId!=null + if (isResumingClaim) { + resumeClaimUseCase + .invoke() + .fold( + ifLeft = { initializing = false - failedToStart = false - claimIntentId = claimIntent.id - steps.clear() - progress = claimIntent.progress - when (val next = claimIntent.next) { - is ClaimIntent.Next.Outcome -> { - outcome = next.claimIntentOutcome + failedToStart = true + }, + ifRight = { claimIntent -> + if (claimIntent==null) { + initializing = false + failedToStart = true + } else { + Snapshot.withMutableSnapshot { + initializing = false + failedToStart = false + claimIntentId = claimIntent.id + steps.clear() + steps.addAll(claimIntent.previousSteps) + progress = claimIntent.progress + when (val next = claimIntent.next) { + is ClaimIntent.Next.Outcome -> { + outcome = next.claimIntentOutcome + } + + is ClaimIntent.Next.Step -> { + steps.add(next.claimIntentStep) + } + } } + } + }, + ) + } else { + startClaimIntentUseCase + .invoke(developmentFlow) + .fold( + ifLeft = { + initializing = false + failedToStart = true + }, + ifRight = { claimIntent -> + Snapshot.withMutableSnapshot { + initializing = false + failedToStart = false + claimIntentId = claimIntent.id + steps.clear() + progress = claimIntent.progress + when (val next = claimIntent.next) { + is ClaimIntent.Next.Outcome -> { + outcome = next.claimIntentOutcome + } - is ClaimIntent.Next.Step -> { - steps.add(next.claimIntentStep) + is ClaimIntent.Next.Step -> { + steps.add(next.claimIntentStep) + } } } - } - }, - ) + }, + ) + } } } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index 6df1fd1eed..d7a6263661 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -15,6 +15,7 @@ internal data class ClaimIntent( val id: ClaimIntentId, val next: Next, val progress: Float?, + val previousSteps: List ) { sealed interface Next { val step: Step? @@ -62,7 +63,10 @@ internal sealed interface StepContent { val uploadUri: String, override val isSkippable: Boolean, val localFiles: List, - ) : StepContent + val remoteFiles: List? + ) : StepContent { + data class RemoteFile(val url: String, val contentType: String, val fileName: String) + } data class Task( val descriptions: List, @@ -83,6 +87,7 @@ internal sealed interface StepContent { val suffix: String?, val title: String, val defaultValues: List, + val currentValues: List, val maxValue: String?, val minValue: String?, val type: FieldType?, @@ -198,7 +203,7 @@ sealed interface AudioRecordingStepState { ) : AudioRecording data class Playback( - val filePath: String, + val audioPath: AudioPath, val isPlaying: Boolean, val isPrepared: Boolean, val hasError: Boolean, @@ -206,6 +211,11 @@ sealed interface AudioRecordingStepState { } } +sealed interface AudioPath { + data class FilePath(val filePath: String): AudioPath + data class RemoteUrl(val remoteUrl: String): AudioPath +} + sealed interface FreeTextErrorType { data class TooShort(val minLength: Int) : FreeTextErrorType } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 9848835497..696435dbaa 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -56,6 +56,9 @@ internal fun ClaimIntentFragment.toClaimIntent(locale: CommonLocale): ClaimInten else -> error("ClaimIntentFragment contained null currentStep and null outcome") }, progress = progress?.toFloat(), + previousSteps = previousSteps.map { + it.toClaimIntentStep(locale) + } ) } @@ -70,6 +73,17 @@ private fun ClaimIntentFragment.CurrentStep.toClaimIntentStep(locale: CommonLoca ) } +context(raise: Raise) +private fun ClaimIntentFragment.PreviousStep.toClaimIntentStep(locale: CommonLocale): ClaimIntentStep { + return ClaimIntentStep( + id = StepId(id), + text = text, + stepContent = this.content.toStepContent(locale), + isRegrettable = this.isRegrettable, + hint = hint, + ) +} + context(raise: Raise) private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): StepContent { return when (this) { @@ -83,7 +97,7 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): is ContentSelectFragment -> { StepContent.ContentSelect( options = options.toOptions(), - selectedOptionId = defaultSelectedId, + selectedOptionId = currentSelectedId ?: defaultSelectedId, isSkippable = isSkippable, style = when (style) { ClaimIntentStepContentSelectStyle.PILL -> StepContent.ContentSelectStyle.PILL @@ -102,10 +116,21 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): } is AudioRecordingFragment -> { + val audioIrl = this.currentAudioUrl + val freeText = this.currentFreeText + val recordingState = if(audioIrl!=null) AudioRecordingStepState.AudioRecording.Playback( + audioPath = AudioPath.RemoteUrl(audioIrl), + isPlaying = false, + isPrepared = true, //TODO: check + hasError = false + ) + //else if (freeText!=null) AudioRecordingStepState.FreeTextDescription() //TODO: fix freeText - move it to + // FreeTextDescription instead of hanging in UiState + else AudioRecordingStepState.AudioRecording.NotRecording StepContent.AudioRecording( uploadUri = uploadUri, isSkippable = isSkippable, - recordingState = AudioRecordingStepState.AudioRecording.NotRecording, + recordingState = recordingState, freeTextMinLength = freeTextMinLength, freeTextMaxLength = freeTextMaxLength, ) @@ -116,6 +141,13 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): uploadUri = uploadUri, isSkippable = isSkippable, localFiles = emptyList(), + remoteFiles = this.currentFiles?.map { + StepContent.FileUpload.RemoteFile( + it.url, + it.contentType, + it.fileName, + ) + } ) } @@ -213,6 +245,7 @@ private fun List.toFields(locale: CommonLocale): List { + return either { + apolloClient + .query( + ResumeClaimQuery(), + ) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .mapLeft { + logcat { "StartClaimIntentUseCase error: $it" } + ClaimChatErrorMessage.GeneralError + } + .bind() + .currentMember.resumableClaimIntent + ?.toClaimIntent(languageService.getLocale()) + } + } +} diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index b998d9d84f..f45c426c66 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -136,10 +136,11 @@ internal fun ClaimChatDestination( isDevelopmentFlow: Boolean, navigateUp: () -> Unit, openPlayStore: () -> Unit, + resumableClaimId: String? ) { val claimChatViewModel = assistedMetroViewModel { - create(isDevelopmentFlow) + create(isDevelopmentFlow, resumableClaimId) } Box(Modifier.fillMaxSize(), propagateMinConstraints = true) { BlurredGradientBackground() diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/UploadFilesStep.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/UploadFilesStep.kt index d1e41861dc..bcd8df47c2 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/UploadFilesStep.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/UploadFilesStep.kt @@ -361,6 +361,7 @@ private fun PreviewUploadFilesStep( id = "1", ), ).takeIf { hasFiles }.orEmpty(), + remoteFiles = null ), appPackageId = "", isCurrentStep = true, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt index 4a063a4a89..1f2fdcfe9e 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt @@ -98,8 +98,10 @@ import com.hedvig.audio.player.data.AudioPlayer import com.hedvig.audio.player.data.AudioPlayerState import com.hedvig.audio.player.data.PlayableAudioSource import com.hedvig.audio.player.data.ProgressPercentage +import com.hedvig.audio.player.data.SignedAudioUrl import com.hedvig.feature.claim.chat.ClaimChatEvent import com.hedvig.feature.claim.chat.FreeTextRestrictions +import com.hedvig.feature.claim.chat.data.AudioPath import com.hedvig.feature.claim.chat.data.AudioRecordingStepState import com.hedvig.feature.claim.chat.data.ClaimIntentStep import com.hedvig.feature.claim.chat.data.FreeTextErrorType @@ -284,9 +286,15 @@ internal fun AudioRecorderBubble( } } else { if (recordingState is AudioRecordingStepState.AudioRecording.Playback) { - val audioPlayer = rememberAudioPlayer( - PlayableAudioSource.LocalFilePath(recordingState.filePath), - ) + val audioPlayer = when (recordingState.audioPath) { + is AudioPath.FilePath -> rememberAudioPlayer( + PlayableAudioSource.LocalFilePath(recordingState.audioPath.filePath)) + is AudioPath.RemoteUrl -> rememberAudioPlayer( + PlayableAudioSource.RemoteUrl( + SignedAudioUrl.fromSignedAudioUrlString(recordingState.audioPath.remoteUrl), + ), + ) + } HedvigAudioPlayer( audioPlayer = audioPlayer, Modifier.padding(start = sentAnswersStartPadding), @@ -355,9 +363,11 @@ private fun AudioRecordingBottomSheet( } val audioPlayer = (audioRecordingState as? AudioRecordingStepState.AudioRecording.Playback)?.let { - rememberAudioPlayer( - PlayableAudioSource.LocalFilePath(it.filePath), - ) + if (it.audioPath is AudioPath.FilePath) { + rememberAudioPlayer( + PlayableAudioSource.LocalFilePath(it.audioPath.filePath), + ) + } else null } LaunchedEffect(bottomSheetState.isVisible) { @@ -1156,19 +1166,19 @@ private class AudioRecordingSheetContentStateProvider : filePath = "/path/to/recording.mp4", ), AudioRecordingStepState.AudioRecording.Playback( - filePath = "/path/to/recording.mp4", + audioPath = AudioPath.FilePath("/path/to/recording.mp4"), isPlaying = false, isPrepared = true, hasError = false, ), AudioRecordingStepState.AudioRecording.Playback( - filePath = "/path/to/recording.mp4", + audioPath = AudioPath.FilePath("/path/to/recording.mp4"), isPlaying = false, isPrepared = false, hasError = false, ), AudioRecordingStepState.AudioRecording.Playback( - filePath = "/path/to/recording.mp4", + audioPath = AudioPath.FilePath("/path/to/recording.mp4"), isPlaying = false, isPrepared = false, hasError = true, From 4664c5bdeec59a56b62cd0c73ae962f7eb9f8435 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 16:17:29 +0200 Subject: [PATCH 03/28] add todo --- .../kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index d7a6263661..c24e3ad222 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -63,7 +63,7 @@ internal sealed interface StepContent { val uploadUri: String, override val isSkippable: Boolean, val localFiles: List, - val remoteFiles: List? + val remoteFiles: List? //TODO: reuse!! ) : StepContent { data class RemoteFile(val url: String, val contentType: String, val fileName: String) } From 4ef191e9735bd019fae6c075279cb40bbcab3f0f Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 16:41:28 +0200 Subject: [PATCH 04/28] use default values --- .../feature/claim/chat/ClaimChatViewModel.kt | 4 +++- .../feature/claim/chat/data/ClaimIntent.kt | 4 +--- .../feature/claim/chat/data/ClaimIntentExt.kt | 22 ++++++++++--------- .../claim/chat/ui/step/UploadFilesStep.kt | 1 - .../ui/step/audiorecording/AudioRecorder.kt | 12 +++++++++- 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index fb7d9210a6..d6849ce853 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -282,7 +282,9 @@ internal class ClaimChatPresenter( failedToStart = false claimIntentId = claimIntent.id steps.clear() - steps.addAll(claimIntent.previousSteps) + steps.addAll(claimIntent.previousSteps.filter { + it.stepContent !is StepContent.Task + }) progress = claimIntent.progress when (val next = claimIntent.next) { is ClaimIntent.Next.Outcome -> { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index c24e3ad222..498b07856b 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -62,8 +62,7 @@ internal sealed interface StepContent { data class FileUpload( val uploadUri: String, override val isSkippable: Boolean, - val localFiles: List, - val remoteFiles: List? //TODO: reuse!! + val localFiles: List ) : StepContent { data class RemoteFile(val url: String, val contentType: String, val fileName: String) } @@ -87,7 +86,6 @@ internal sealed interface StepContent { val suffix: String?, val title: String, val defaultValues: List, - val currentValues: List, val maxValue: String?, val minValue: String?, val type: FieldType?, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 696435dbaa..1e925e63cc 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -4,6 +4,7 @@ import arrow.core.raise.Raise import arrow.core.raise.context.raise import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.locale.CommonLocale +import com.hedvig.android.core.uidata.UiFile import com.hedvig.android.design.system.hedvig.DatePickerUiState import com.hedvig.android.logger.logcat import com.hedvig.android.shared.partners.deflect.DeflectData @@ -140,14 +141,15 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): StepContent.FileUpload( uploadUri = uploadUri, isSkippable = isSkippable, - localFiles = emptyList(), - remoteFiles = this.currentFiles?.map { - StepContent.FileUpload.RemoteFile( - it.url, - it.contentType, - it.fileName, - ) - } + localFiles = this.currentFiles?.map { + UiFile( + name = it.fileName, + localPath = null, + url = it.url, + mimeType = it.contentType, + id = it.url, + ) + } ?: emptyList() ) } @@ -244,8 +246,8 @@ private fun List.toFields(locale: CommonLocale): List rememberAudioPlayer( + PlayableAudioSource.LocalFilePath(uiState.audioPath.filePath)) + is AudioPath.RemoteUrl -> rememberAudioPlayer( + PlayableAudioSource.RemoteUrl( + SignedAudioUrl.fromSignedAudioUrlString(uiState.audioPath.remoteUrl), + ), + ) + } if (!isCurrentStep) { HedvigAudioPlayer( audioPlayer = audioPlayer, From 85bc292a9cea19ad2730eba96131e642dee45d17 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 16:58:53 +0200 Subject: [PATCH 05/28] use default values better --- .../com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt | 9 +++++---- .../com/hedvig/feature/claim/chat/ui/step/FormStep.kt | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index 1e925e63cc..fcc2ab8a7d 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -241,13 +241,14 @@ private fun List.toOptions(): List) private fun List.toFields(locale: CommonLocale): List { return this.map { field -> + val defaultValues = if(field.currentValues.isNotEmpty()) field.currentValues.toFieldOptions(field.options) + else field.defaultValues.toFieldOptions(field.options) StepContent.Form.Field( id = FieldId(field.id), isRequired = field.isRequired, suffix = field.suffix, title = field.title, - defaultValues = if(field.currentValues.isNotEmpty()) field.currentValues.toFieldOptions(field.options) - else field.defaultValues.toFieldOptions(field.options), + defaultValues = defaultValues, maxValue = field.maxValue, minValue = field.minValue, type = when (field.type) { @@ -295,12 +296,12 @@ private fun List.toFields(locale: CommonLocale): List { DatePickerUiState( locale = locale, - initiallySelectedDate = field.defaultValues.getOrNull(0)?.let { LocalDate.parse(it) }, + initiallySelectedDate = defaultValues.getOrNull(0)?.let { LocalDate.parse(it.text) }, minDate = field.minValue?.let { LocalDate.parse(it) } ?: LocalDate(1900, 1, 1), maxDate = field.maxValue?.let { LocalDate.parse(it) } ?: LocalDate(2100, 1, 1), ) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/FormStep.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/FormStep.kt index 512c1689f7..b0da4e687f 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/FormStep.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/FormStep.kt @@ -190,7 +190,8 @@ private fun FormContent( FieldType.TEXT -> { TextInputBubble( questionLabel = field.title, - text = field.selectedOptions.getOrNull(0)?.text, + text = field.selectedOptions.getOrNull(0)?.text + ?: field.defaultValues.getOrNull(0)?.text, suffix = field.suffix, onInput = { answer -> onSelectFieldAnswer( From 3c6f5908406980fbcd47094a17f3dad744d580bc Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Tue, 30 Jun 2026 14:10:32 +0200 Subject: [PATCH 06/28] move freeText to stepContent instead of singleton in ui state --- .../claim/chat/navigation/ClaimChatEntries.kt | 4 +- .../feature/claim/chat/ClaimChatViewModel.kt | 40 +++++++------- .../feature/claim/chat/data/ClaimIntent.kt | 10 ++-- .../feature/claim/chat/data/ClaimIntentExt.kt | 52 +++++++++++-------- .../claim/chat/ui/ClaimChatDestination.kt | 13 +++-- .../claim/chat/ui/ClaimChatUiComponents.kt | 2 +- .../claim/chat/ui/StartClaimPledgeScreen.kt | 14 ++--- .../ui/step/audiorecording/AudioRecorder.kt | 4 +- .../AudioRecordingStepSections.kt | 13 ++--- 9 files changed, 77 insertions(+), 75 deletions(-) diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt index 7ca634c95f..efa88f89da 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt @@ -38,7 +38,7 @@ internal data class ClaimOutcomeNewClaimKey( internal data object UpdateAppKey : HedvigNavKey @Serializable -internal data object StartClaimPledgeKey: HedvigNavKey +internal data object StartClaimPledgeKey : HedvigNavKey fun EntryProviderScope.claimChatEntries( backstack: Backstack, @@ -112,7 +112,7 @@ fun EntryProviderScope.claimChatEntries( ClaimChatKey(), inclusive = true, ) - } + }, ) } } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index d6849ce853..9740ad56ea 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -148,7 +148,6 @@ internal sealed interface ClaimChatUiState { val claimIntentId: ClaimIntentId, val steps: List, val currentStep: ClaimIntentStep?, - val freeText: String?, val outcome: ClaimIntentOutcome?, val errorSubmittingStep: ClaimChatErrorMessage?, val currentContinueButtonLoading: Boolean = false, @@ -179,7 +178,7 @@ internal class ClaimChatViewModel( regretStepUseCase: RegretStepUseCase, formFieldSearchUseCase: FormFieldSearchUseCase, fileService: FileService, - resumeClaimUseCase: ResumeClaimUseCase + resumeClaimUseCase: ResumeClaimUseCase, ) : MoleculeViewModel( ClaimChatUiState.Initializing, ClaimChatPresenter( @@ -198,7 +197,7 @@ internal class ClaimChatViewModel( regretStepUseCase, formFieldSearchUseCase, resumableClaimId, - resumeClaimUseCase + resumeClaimUseCase, ), ) { override fun onCleared() { @@ -223,7 +222,7 @@ internal class ClaimChatPresenter( private val regretStepUseCase: RegretStepUseCase, private val formFieldSearchUseCase: FormFieldSearchUseCase, private val resumableClaimId: String?, - private val resumeClaimUseCase: ResumeClaimUseCase + private val resumeClaimUseCase: ResumeClaimUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: ClaimChatUiState): ClaimChatUiState { @@ -247,7 +246,6 @@ internal class ClaimChatPresenter( var currentContinueButtonLoading by remember { mutableStateOf(false) } var currentSkipButtonLoading by remember { mutableStateOf(false) } var errorSubmittingStep by remember { mutableStateOf(null) } - var freeText by remember { mutableStateOf(null) } var showConfirmEditDialogForStep by remember { mutableStateOf(null) } var progress by remember { mutableStateOf( @@ -261,9 +259,9 @@ internal class ClaimChatPresenter( var searchQuery by remember { mutableStateOf(null) } - if (initializing) { //TODO + if (initializing) { LaunchedEffect(Unit) { - val isResumingClaim = resumableClaimId!=null + val isResumingClaim = resumableClaimId != null if (isResumingClaim) { resumeClaimUseCase .invoke() @@ -273,7 +271,7 @@ internal class ClaimChatPresenter( failedToStart = true }, ifRight = { claimIntent -> - if (claimIntent==null) { + if (claimIntent == null) { initializing = false failedToStart = true } else { @@ -282,9 +280,11 @@ internal class ClaimChatPresenter( failedToStart = false claimIntentId = claimIntent.id steps.clear() - steps.addAll(claimIntent.previousSteps.filter { - it.stepContent !is StepContent.Task - }) + steps.addAll( + claimIntent.previousSteps.filter { + it.stepContent !is StepContent.Task + }, + ) progress = claimIntent.progress when (val next = claimIntent.next) { is ClaimIntent.Next.Outcome -> { @@ -476,7 +476,10 @@ internal class ClaimChatPresenter( } is ClaimChatEvent.AudioRecording.SubmitTextInput -> { - val freeTextInput = freeText ?: return@CollectEvents + val recordingState = steps.find { it.id == event.id } + ?.stepContent.let { it as? StepContent.AudioRecording } + ?.recordingState as? FreeTextDescription + val freeTextInput = recordingState?.freeText ?: return@CollectEvents currentContinueButtonLoading = true launch { submitAudioRecordingUseCase @@ -524,13 +527,7 @@ internal class ClaimChatPresenter( } is ClaimChatEvent.AudioRecording.SwitchToFreeText -> { - val currentContent = currentStep?.stepContent as? StepContent.AudioRecording - ?: return@CollectEvents - val textTooShort = freeText?.length?.let { - currentContent.freeTextMinLength > it - } ?: true steps.updateStepWithSuccess(event.id) { step, content -> - val canSubmit = !currentContinueButtonLoading && !freeText.isNullOrEmpty() && !textTooShort showFreeTextOverlay = FreeTextRestrictions( content.freeTextMinLength, content.freeTextMaxLength, @@ -539,7 +536,8 @@ internal class ClaimChatPresenter( stepContent = content.copy( recordingState = FreeTextDescription( errorType = null, - canSubmit = canSubmit, + canSubmit = false, + freeText = null, ), ), ) @@ -735,11 +733,11 @@ internal class ClaimChatPresenter( null }, canSubmit = canSubmit, + freeText = event.text, ), ), ) } - freeText = event.text } } @@ -788,7 +786,6 @@ internal class ClaimChatPresenter( val index = steps.indexOf(stepToUpdate) if (index >= 0) { steps.subList(index, steps.size).clear() - if (steps.none { it.stepContent is StepContent.AudioRecording }) freeText = null } currentContinueButtonLoading = false currentSkipButtonLoading = false @@ -995,7 +992,6 @@ internal class ClaimChatPresenter( currentStep = currentStep, outcome = outcome, showFreeTextOverlay = showFreeTextOverlay, - freeText = freeText, errorSubmittingStep = errorSubmittingStep, currentContinueButtonLoading = currentContinueButtonLoading, currentSkipButtonLoading = currentSkipButtonLoading, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index 498b07856b..b36c5c229c 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -15,7 +15,7 @@ internal data class ClaimIntent( val id: ClaimIntentId, val next: Next, val progress: Float?, - val previousSteps: List + val previousSteps: List, ) { sealed interface Next { val step: Step? @@ -62,7 +62,7 @@ internal sealed interface StepContent { data class FileUpload( val uploadUri: String, override val isSkippable: Boolean, - val localFiles: List + val localFiles: List, ) : StepContent { data class RemoteFile(val url: String, val contentType: String, val fileName: String) } @@ -189,6 +189,7 @@ sealed interface AudioRecordingStepState { val errorType: FreeTextErrorType?, val canSubmit: Boolean, val hasError: Boolean = false, + val freeText: String? = null, ) : AudioRecordingStepState sealed interface AudioRecording : AudioRecordingStepState { @@ -210,8 +211,9 @@ sealed interface AudioRecordingStepState { } sealed interface AudioPath { - data class FilePath(val filePath: String): AudioPath - data class RemoteUrl(val remoteUrl: String): AudioPath + data class FilePath(val filePath: String) : AudioPath + + data class RemoteUrl(val remoteUrl: String) : AudioPath } sealed interface FreeTextErrorType { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index fcc2ab8a7d..b9e7a14aa4 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -59,7 +59,7 @@ internal fun ClaimIntentFragment.toClaimIntent(locale: CommonLocale): ClaimInten progress = progress?.toFloat(), previousSteps = previousSteps.map { it.toClaimIntentStep(locale) - } + }, ) } @@ -117,17 +117,24 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): } is AudioRecordingFragment -> { - val audioIrl = this.currentAudioUrl + val audioUrl = this.currentAudioUrl val freeText = this.currentFreeText - val recordingState = if(audioIrl!=null) AudioRecordingStepState.AudioRecording.Playback( - audioPath = AudioPath.RemoteUrl(audioIrl), - isPlaying = false, - isPrepared = true, //TODO: check - hasError = false - ) - //else if (freeText!=null) AudioRecordingStepState.FreeTextDescription() //TODO: fix freeText - move it to - // FreeTextDescription instead of hanging in UiState - else AudioRecordingStepState.AudioRecording.NotRecording + val recordingState = if (audioUrl != null) { + AudioRecordingStepState.AudioRecording.Playback( + audioPath = AudioPath.RemoteUrl(audioUrl), + isPlaying = false, + isPrepared = true, // TODO: check + hasError = false, + ) + } else if (freeText != null) { + AudioRecordingStepState.FreeTextDescription( + errorType = null, + canSubmit = true, + freeText = freeText, + ) + } else { + AudioRecordingStepState.AudioRecording.NotRecording + } StepContent.AudioRecording( uploadUri = uploadUri, isSkippable = isSkippable, @@ -142,14 +149,14 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): uploadUri = uploadUri, isSkippable = isSkippable, localFiles = this.currentFiles?.map { - UiFile( - name = it.fileName, - localPath = null, - url = it.url, - mimeType = it.contentType, - id = it.url, - ) - } ?: emptyList() + UiFile( + name = it.fileName, + localPath = null, + url = it.url, + mimeType = it.contentType, + id = it.url, + ) + } ?: emptyList(), ) } @@ -241,8 +248,11 @@ private fun List.toOptions(): List) private fun List.toFields(locale: CommonLocale): List { return this.map { field -> - val defaultValues = if(field.currentValues.isNotEmpty()) field.currentValues.toFieldOptions(field.options) - else field.defaultValues.toFieldOptions(field.options) + val defaultValues = if (field.currentValues.isNotEmpty()) { + field.currentValues.toFieldOptions(field.options) + } else { + field.defaultValues.toFieldOptions(field.options) + } StepContent.Form.Field( id = FieldId(field.id), isRequired = field.isRequired, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index f45c426c66..a7b7d217e9 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -88,6 +88,7 @@ import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.* import com.hedvig.feature.claim.chat.ClaimChatUiState import com.hedvig.feature.claim.chat.ClaimChatViewModel import com.hedvig.feature.claim.chat.ClaimChatViewModelFactory +import com.hedvig.feature.claim.chat.data.AudioRecordingStepState import com.hedvig.feature.claim.chat.data.ClaimChatErrorMessage import com.hedvig.feature.claim.chat.data.ClaimIntentOutcome import com.hedvig.feature.claim.chat.data.ClaimIntentStep @@ -136,7 +137,7 @@ internal fun ClaimChatDestination( isDevelopmentFlow: Boolean, navigateUp: () -> Unit, openPlayStore: () -> Unit, - resumableClaimId: String? + resumableClaimId: String?, ) { val claimChatViewModel = assistedMetroViewModel { @@ -228,9 +229,12 @@ private fun ClaimChatScreen( openAppSettings: () -> Unit, openPlayStore: () -> Unit, ) { + val currentFreeText = (uiState.currentStep?.stepContent as? StepContent.AudioRecording) + ?.recordingState.let { it as? AudioRecordingStepState.FreeTextDescription } + ?.freeText FreeTextOverlay( freeTextMaxLength = uiState.showFreeTextOverlay?.maxLength ?: 2000, - freeTextValue = uiState.freeText, + freeTextValue = currentFreeText, freeTextHint = stringResource(Res.string.CLAIMS_TEXT_INPUT_POPOVER_PLACEHOLDER), freeTextTitle = stringResource(Res.string.CLAIMS_TEXT_INPUT_PLACEHOLDER), freeTextOnCancelClick = { @@ -486,7 +490,6 @@ private fun ClaimChatScrollableContent( StepContentSection( stepItem = item, - freeText = uiState.freeText, isCurrentStep = isCurrentStep, showAnimationSequence = showAnimationSequence, currentContinueButtonLoading = uiState.currentContinueButtonLoading, @@ -544,7 +547,6 @@ private fun ScrollToBottomButton(onClick: () -> Unit, modifier: Modifier = Modif @Composable private fun StepContentSection( stepItem: ClaimIntentStep, - freeText: String?, isCurrentStep: Boolean, showAnimationSequence: Boolean, currentContinueButtonLoading: Boolean, @@ -620,7 +622,6 @@ private fun StepContentSection( ) { StepBottomContent( stepItem = stepItem, - freeText = freeText, isCurrentStep = isCurrentStep, currentContinueButtonLoading = currentContinueButtonLoading, currentSkipButtonLoading = currentSkipButtonLoading, @@ -757,7 +758,6 @@ private fun CommonPaddingWrapper(content: @Composable () -> Unit) { @Composable private fun StepBottomContent( stepItem: ClaimIntentStep, - freeText: String?, isCurrentStep: Boolean, currentContinueButtonLoading: Boolean, currentSkipButtonLoading: Boolean, @@ -808,7 +808,6 @@ private fun StepBottomContent( clock = Clock.System, onShouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale, openAppSettings = openAppSettings, - freeText = freeText, onEvent = onEvent, continueButtonLoading = currentContinueButtonLoading, skipButtonLoading = currentSkipButtonLoading, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt index 5f6836c811..446d357903 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatUiComponents.kt @@ -43,6 +43,7 @@ private fun PreviewClaimChatComponents() { recordingState = AudioRecordingStepState.FreeTextDescription( errorType = null, canSubmit = true, + freeText = "some not really long free text", ), clock = Clock.System, onShouldShowRequestPermissionRationale = { @@ -60,7 +61,6 @@ private fun PreviewClaimChatComponents() { onLaunchFullScreenEditText = {}, canSkip = true, onSkip = {}, - freeText = "some not really long free text", continueButtonLoading = false, skipButtonLoading = false, ) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/StartClaimPledgeScreen.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/StartClaimPledgeScreen.kt index 543d1c9100..e0a9bc96f8 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/StartClaimPledgeScreen.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/StartClaimPledgeScreen.kt @@ -25,10 +25,7 @@ import hedvig.resources.Res import org.jetbrains.compose.resources.stringResource @Composable -internal fun StartClaimPledgeDestination( - navigateUp: () -> Unit, - navigateToClaimChat: () -> Unit -) { +internal fun StartClaimPledgeDestination(navigateUp: () -> Unit, navigateToClaimChat: () -> Unit) { Surface( color = HedvigTheme.colorScheme.backgroundPrimary, ) { @@ -47,21 +44,18 @@ internal fun StartClaimPledgeDestination( navigateToClaimChat = navigateToClaimChat, modifier = Modifier .weight(1f) - .padding(horizontal = 16.dp) + .padding(horizontal = 16.dp), ) } } } - @HedvigShortMultiScreenPreview @Composable -private fun PreviewStartClaimPledgeDestination( -) { +private fun PreviewStartClaimPledgeDestination() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { - StartClaimPledgeDestination({},{} - ) + StartClaimPledgeDestination({}, {}) } } } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecorder.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecorder.kt index d1301639fe..8209440bb3 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecorder.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecorder.kt @@ -255,8 +255,8 @@ private fun Playback( HedvigCircularProgressIndicator() } else { val audioPlayer = when (uiState.audioPath) { - is AudioPath.FilePath -> rememberAudioPlayer( - PlayableAudioSource.LocalFilePath(uiState.audioPath.filePath)) + is AudioPath.FilePath -> rememberAudioPlayer(PlayableAudioSource.LocalFilePath(uiState.audioPath.filePath)) + is AudioPath.RemoteUrl -> rememberAudioPlayer( PlayableAudioSource.RemoteUrl( SignedAudioUrl.fromSignedAudioUrlString(uiState.audioPath.remoteUrl), diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt index 1f2fdcfe9e..34f584d9ea 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/step/audiorecording/AudioRecordingStepSections.kt @@ -142,7 +142,6 @@ import org.jetbrains.compose.resources.stringResource @Composable internal fun AudioRecordingStep( item: ClaimIntentStep, - freeText: String?, stepContent: StepContent.AudioRecording, onShowFreeText: () -> Unit, onSwitchToAudioRecording: () -> Unit, @@ -187,7 +186,6 @@ internal fun AudioRecordingStep( canSkip = stepContent.isSkippable, onSkip = onSkip, isCurrentStep = isCurrentStep, - freeText = freeText, continueButtonLoading = continueButtonLoading, skipButtonLoading = skipButtonLoading, ) @@ -203,7 +201,6 @@ internal fun AudioRecordingStep( @Composable internal fun AudioRecorderBubble( recordingState: AudioRecordingStepState, - freeText: String?, clock: Clock, onShouldShowRequestPermissionRationale: (String) -> Boolean, startRecording: () -> Unit, @@ -240,7 +237,7 @@ internal fun AudioRecorderBubble( submitFreeText = submitFreeText, showAudioRecording = onSwitchToAudioRecording, onLaunchFullScreenEditText = onLaunchFullScreenEditText, - freeText = freeText, + freeText = recordingState.freeText, hasError = recordingState.hasError, errorType = recordingState.errorType, isCurrentStep = isCurrentStep, @@ -288,7 +285,9 @@ internal fun AudioRecorderBubble( if (recordingState is AudioRecordingStepState.AudioRecording.Playback) { val audioPlayer = when (recordingState.audioPath) { is AudioPath.FilePath -> rememberAudioPlayer( - PlayableAudioSource.LocalFilePath(recordingState.audioPath.filePath)) + PlayableAudioSource.LocalFilePath(recordingState.audioPath.filePath), + ) + is AudioPath.RemoteUrl -> rememberAudioPlayer( PlayableAudioSource.RemoteUrl( SignedAudioUrl.fromSignedAudioUrlString(recordingState.audioPath.remoteUrl), @@ -367,7 +366,9 @@ private fun AudioRecordingBottomSheet( rememberAudioPlayer( PlayableAudioSource.LocalFilePath(it.audioPath.filePath), ) - } else null + } else { + null + } } LaunchedEffect(bottomSheetState.isVisible) { From bde0ea61a2ea4ffe563d7dde5eb470f070125a67 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 1 Jul 2026 09:43:45 +0200 Subject: [PATCH 07/28] test add missing param --- .../feature/home/home/ui/HomePresenterTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 4f7fde4823..c122a4ee87 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -143,6 +143,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(testCrossSell, listOf()), firstVetSections = listOf(), travelBannerInfo = null, + resumableClaimId = null ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -174,6 +175,8 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -207,6 +210,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -225,6 +230,8 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -282,6 +289,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf()), travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()) @@ -317,6 +326,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -333,6 +344,8 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -371,6 +384,8 @@ internal class HomePresenterTest { ), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -387,6 +402,8 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -424,6 +441,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -444,6 +463,7 @@ internal class HomePresenterTest { ), addonBannerInfo = null, isProduction = false, + resumableClaimId = null ), ) } @@ -474,6 +494,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -490,6 +512,8 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -520,6 +544,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + resumableClaimId = null + ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -536,6 +562,8 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, + resumableClaimId = null + ), ) } @@ -562,6 +590,8 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList()), travelBannerInfo = null, + resumableClaimId = null + ) } From c1172815f6c5dbd6783febe6ce93282f803c4300 Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Wed, 1 Jul 2026 11:56:49 +0200 Subject: [PATCH 08/28] todo text in dialog --- .../com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index a7b7d217e9..6c3d602e1a 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -323,7 +323,8 @@ private fun ClaimChatScreenContent( if (showCloseFlowDialog) { HedvigAlertDialog( title = stringResource(Res.string.GENERAL_ARE_YOU_SURE), - text = stringResource(Res.string.claims_alert_body), + // text = stringResource(Res.string.claims_alert_body), + text = "Your answers will be saved in a draft claim", //TODO onDismissRequest = { showCloseFlowDialog = false }, From 569af46cba098c052ba35fea71014e9ea05d44e8 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 15:32:56 +0200 Subject: [PATCH 09/28] Add resume-draft-claim design spec --- .../2026-07-03-resume-draft-claim-design.md | 251 ++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-03-resume-draft-claim-design.md diff --git a/docs/superpowers/specs/2026-07-03-resume-draft-claim-design.md b/docs/superpowers/specs/2026-07-03-resume-draft-claim-design.md new file mode 100644 index 0000000000..6c8a6b88dc --- /dev/null +++ b/docs/superpowers/specs/2026-07-03-resume-draft-claim-design.md @@ -0,0 +1,251 @@ +# Resume draft claim: Android parity with iOS PR #2434 + +Date: 2026-07-03 +Branch: `feat-resume-claim` +References: iOS PR HedvigInsurance/ugglan#2434 (final state as of 2026-07-03, including the +"Added flag" and "Added expired alert" commits), backend schema fields +`ClaimIntent.resumable: Boolean!` and `claimIntentDeleteDraft(id: ID!): Boolean!`, and the +`RESUME_CLAIM_*` Lokalise string batch. + +## Goal + +Bring the Android `feat-resume-claim` branch to behavioral parity with the final state of the +iOS resume-claim PR and the current backend schema. The branch already implements the resume +mechanics inside the claim chat (previous-step replay, seeding of `current*` values, the +free-text-per-step fix). This spec covers the remaining gaps: + +1. A Draft card in the home claim-cards carousel, always first, with Delete + Continue. +2. A "You have a draft claim" dialog when starting a new claim while a draft exists, + replacing the current in-sheet "Continue with the draft claim" button. +3. The claim chat leave dialog gated on the new `ClaimIntent.resumable` field, with new copy. +4. The claim chat title taken from `ClaimIntent.displayName`. +5. The inbox start-claim entry point gets the same draft handling as home. + +Decisions already made with the user: + +- Full parity with iOS, including the draft card. +- Everything is gated behind the Unleash flag `enable_claim_intent_resume`, matching iOS. +- Shared draft-claim data goes in a new small KMP data module (Approach A), not per-feature + duplication and not an observable store singleton. +- All new user-facing copy uses the `RESUME_CLAIM_*` Lokalise string resources (already + uploaded to Lokalise; run `./gradlew downloadStrings` and verify the keys are present + before implementation). No hardcoded strings. + +## Section 0: Feature flag + +Add `ENABLE_CLAIM_INTENT_RESUME` to the `Feature` enum (commonMain), mapped to the raw +Unleash key `enable_claim_intent_resume` in `Feature.unleashKey` (androidMain), per +`FEATURE_FLAG_DEFAULTS.md`. Positive polarity; no bootstrap entry needed: when Unleash has +never been fetched the flag resolves to off, which safely hides the feature (it does not +gate anything the app needs to function). + +Gating points (all matching iOS): + +- **Home:** the Home query's `resumableClaimIntent` selection gets + `@include(if: $resumeClaimEnabledFlag)`, following the existing `$claimsHistoryFlag` + variable pattern in `GetHomeDataUseCaseImpl`. Flag off means `draftClaim == null`: + no card, no draft dialog. +- **Inbox:** `InboxPresenter` skips the `GetResumableClaimIntentUseCase` fetch when the + flag is off (treats it as no draft). +- **Claim chat title:** `displayName` is used only when the flag is on; otherwise the + legacy `CHAT_CONVERSATION_CLAIM_TITLE` string. +- **Claim chat leave dialog:** flag on gives the new resumable-gated behavior (Section 4); + flag off keeps the legacy behavior of always confirming dismissal with the generic + claims alert copy (`claims_alert_body`). +- The claim chat presenter/destination reads the flag via an injected `FeatureManager` + (same pattern as other features). + +## Section 1: Schema and shared data module + +**Schema:** commit the already-downloaded `schema.graphqls` diff (adds `ClaimIntent.resumable` +and the `claimIntentDeleteDraft` mutation) as its own "Download schema" commit, matching the +existing commit convention. + +**New module `app/data/data-claim-intent`** (KMP, `hedvig.multiplatform.library` + +`hedvig.gradle.plugin`, `hedvig { apollo("octopus") }`). Auto-discovered by settings. Note: +`app/data/data-claim-flow` and `app/data/data-claim-triaging` are empty leftover directories; +this module is new, not a revival of those. + +Public API (no `octopus.*` types leak): + +```kotlin +data class ResumableClaimIntent( + val id: String, + val displayName: String?, + val startedAt: LocalDate, // from ClaimIntent.createdAt +) + +interface GetResumableClaimIntentUseCase { + suspend fun invoke(): Either // null = no draft +} + +interface DeleteClaimIntentDraftUseCase { + suspend fun invoke(id: String): Either +} +``` + +- `GetResumableClaimIntentUseCaseImpl` runs + `query ResumableClaimIntent { currentMember { resumableClaimIntent { id displayName createdAt } } }` + with `FetchPolicy.NetworkOnly`. +- `DeleteClaimIntentDraftUseCaseImpl` runs `mutation { claimIntentDeleteDraft(id: $id) }` and + `ensure`s the returned Boolean is true. +- Both impls are `internal`, bound with `@ContributesBinding(AppScope::class)`. +- `feature-home` and `feature-chat` add `implementation(projects.dataClaimIntent)`. +- `feature-claim-chat` keeps its own internal full `ResumeClaimQuery` (it needs the whole step + fragment, which is feature-internal) and does not depend on the new module. + +## Section 2: Home draft card + +**Home query** (`app/feature/feature-home/src/main/graphql/QueryHome.graphql`): extend the +existing embedded selection to `resumableClaimIntent { id displayName createdAt }`. The card +data rides along with the rest of home: one network call, refreshes with home reload. + +**`HomeData`** (`GetHomeDataUseCase.kt`): replace `resumableClaimId: String?` with +`draftClaim: DraftClaim?` where `DraftClaim(id: String, displayName: String?, startedAt: LocalDate)` +(project-owned, mapped from the query). Demo use case returns `null`. + +**Card UI in `app/ui/claim-status`** (`ClaimStatusCards.kt`): introduce a sealed list item so +the existing pager hosts both card kinds: + +```kotlin +sealed interface ClaimCardUiState { + data class Claim(val uiState: ClaimStatusCardUiState) : ClaimCardUiState + data class Draft(val id: String, val title: String?, val startedAt: LocalDate) : ClaimCardUiState +} +``` + +- `ClaimStatusCards` takes `NonEmptyList` plus `onContinueDraftClaim` and + `onDeleteDraftClaim` lambdas. Existing single-card call sites that render a claim directly + (for example claim details) keep using the inner `ClaimStatusCard` composable and are untouched. +- New `DraftClaimCard` composable, per the iOS screenshots: "Draft" highlight pill in the + amber style (`RESUME_CLAIM_DRAFT`), title = `title ?: RESUME_CLAIM_FALLBACK_TITLE` + ("Continue where you stopped", the effective iOS fallback), subtitle + `RESUME_CLAIM_STATED` formatted with the started date ("Started %1$@"), the three progress + segments ("Started", "Being handled", "Closed") rendered in the disabled/inactive style, + and a button row of Delete (secondary, `RESUME_CLAIM_DELETE_BUTTON`) + Continue (primary, + `RESUME_CLAIM_CONTINUE_BUTTON`). + +**Ordering:** home builds the pager list with the draft first, then active claims (per the +Slack thread: draft is always first, even when a claim updates). + +**Delete flow:** Delete opens a `HedvigAlertDialog` (`RESUME_CLAIM_DELETE_TITLE` / +`RESUME_CLAIM_DELETE_BODY` / cancel = `general_cancel_button`, confirm = +`RESUME_CLAIM_DELETE_BUTTON`). Confirm sends a new `HomeEvent.DeleteDraftClaim` to +`HomePresenter`, which calls `DeleteClaimIntentDraftUseCase(id)` and, on success, triggers +the existing reload path so the card disappears. On failure: log only (matches iOS's silent +catch); no new error UI. + +**Expired drafts (matches iOS "Added expired alert"):** `DraftClaim` exposes +`fun isExpired(clock: Clock): Boolean`, true when today is past `startedAt + 7 days` +(client-side, same heuristic as iOS; the backend copy says drafts live 7 days). When the +card's Continue button is tapped on an expired draft, show an alert +(`RESUME_CLAIM_EXPIRED_TITLE` / `RESUME_CLAIM_EXPIRED_BODY`, single dismiss button) instead +of navigating. Only the card's Continue checks expiry, matching iOS. + +**Continue flow (non-expired):** navigates straight into the resumed claim chat, skipping the +honesty pledge (matches iOS). + +**Refresh on return:** verify that home refetches when the user comes back from the claim chat +(iOS refetches explicitly on flow dismissal). If home does not already reload on re-entry, +trigger the reload when returning from the claim flow so a freshly abandoned claim shows its +draft card immediately. + +## Section 3: Start-claim entry points (home + inbox) + +**Design system `StartClaimBottomSheet.kt`:** + +- Revert the sheet to `HedvigBottomSheetState`; delete `StartClaimSheetData` and the + in-sheet "Continue with the draft claim" button (iOS ended up not putting resume inside the + pledge sheet). This also reverts the leftover unused parameters on `StartClaimPledgeScreen`. +- Add `DraftClaimDialog` composable next to it: a `HedvigDialog` with three stacked big buttons. + Copy: title `RESUME_CLAIM_DRAFT_ALERT_TITLE`, body `RESUME_CLAIM_DRAFT_ALERT_BODY`, buttons + `RESUME_CLAIM_DRAFT_ALERT_CONTINUE`, `RESUME_CLAIM_DRAFT_ALERT_START_NEW` (red/attention + text style, per screenshot), `general_cancel_button`. + +**Flow on both screens (matches iOS):** on tapping "Make a claim" (home) or "Start claim" +(inbox's `NewChatSelectBottomSheetContent`): + +- No draft: open the pledge sheet directly (unchanged). +- Draft exists: show `DraftClaimDialog` first. + - "Continue draft": navigate straight into the resumed claim chat (no pledge). + - "Start new claim": open the pledge sheet (plain new-claim flow). + - "Cancel": dismiss, nothing else. + +**Home wiring** (`HomeDestination.kt`): branch on `draftClaim != null` before +`startClaimBottomSheetState.show(Unit)`. + +**Inbox wiring** (`InboxViewModel.kt` / `InboxDestination.kt`): `InboxPresenter` injects +`GetResumableClaimIntentUseCase` and fetches it alongside conversations on load/reload, +exposing the draft (or just its presence) in `InboxUiState`. `InboxDestination` branches the +same way home does. `inboxEntries` and `HedvigEntryProvider` thread a resume-capable +navigate lambda. + +**Navigation key** (`ClaimChatEntries.kt`): `ClaimChatKey.resumableClaimId: String?` becomes +`resumeClaim: Boolean = false`. The resume query takes no id (a member has at most one draft; +iOS passes no id either), and a stale id after process-death restore would be misleading. +`navigateToClaimChat` lambdas change from `(String?) -> Unit` to `(resumeClaim: Boolean) -> Unit`. +`ClaimChatViewModel`/`ClaimChatPresenter` assisted param changes accordingly. + +## Section 4: Claim chat screen + +**Fragment** (`FragmentClaimIntent.graphql`): add `displayName` and `resumable` to +`ClaimIntentFragment`. Map both into the internal `ClaimIntent` model and into +`ClaimChatUiState.InProgress` as `title: String?` and `isResumable: Boolean`. + +**Title:** with the flag on, the `TopAppBar` uses `displayName` when present, falling back to +the existing `CHAT_CONVERSATION_CLAIM_TITLE` string ("My things", "My trip • Sickness" on +iOS). Flag off keeps the legacy title. + +**Leave dialog** (replaces the current always-shown dialog with hardcoded TODO text in +`ClaimChatDestination.kt`): + +- Flag on, `isResumable == true`: dialog with title `RESUME_CLAIM_LEAVE_TITLE`, body + `RESUME_CLAIM_LEAVE_BODY`, dismiss `GENERAL_NO`, confirm `RESUME_CLAIM_LEAVE_CONFIRM`. +- Flag on, `isResumable == false`: no dialog; close immediately (matches iOS's conditional + dismiss). +- Flag off: legacy behavior, always confirm with the generic claims alert copy + (`claims_alert_body`), restoring the string the branch commented out. + +**Deflect resume:** the presenter's resume path already routes `ClaimIntent.Next.Outcome` to +the outcome/deflect screen; verify against the deflect screenshots rather than adding code. + +**Cleanups on the branch:** + +- Resolve `isPrepared = true, // TODO: check` in `ClaimIntentExt.kt` for resumed remote audio: + verify remote playback via `PlayableAudioSource.RemoteUrl` and either keep the value with a + real rationale or fix it. Note `AudioRecordingBottomSheet` returns a `null` player for remote + URLs; that path is only reachable pre-submit, verify and leave as is if confirmed. +- The commented-out `// text = stringResource(Res.string.claims_alert_body)` line and its + TODO replacement disappear into the flag-dependent leave-dialog logic above. +- Run `./gradlew ktlintFormat` over the branch (style drift exists: `resumableClaimId!=null`, + missing trailing commas, odd line breaks in `StartClaimBottomSheet.kt`). + +## Testing + +- `HomePresenterTest`: update for `draftClaim` (replacing `resumableClaimId`), add coverage for + the delete-draft event (success reloads, failure keeps state) and for the flag-off case + (no draft in ui state even when the backend would return one). +- Inbox presenter test (if the existing test pattern covers `InboxPresenter`): draft presence + reaches the ui state. +- `ExhaustiveBackStackSerializationTest` picks up the `ClaimChatKey` shape change automatically. +- `feature-claim-chat` presenter test for the resume-seeding path if presenter test infra + exists there: previous steps are replayed with Task steps filtered out, and an + outcome-terminal draft resumes into the outcome. + +## Error handling summary + +- Get resumable intent fails on home: home already surfaces query errors through its existing + error state; the draft card is simply absent when the field is null. +- Get resumable intent fails on inbox: treat as "no draft" (do not block starting a claim). +- Delete draft fails: log, keep the card (silent, matches iOS). +- Draft expired at the card: expired alert on Continue (Section 2); delete still works. +- Resume query returns null (draft expired/deleted between card render and entering the + chat): the existing `failedToStart` error state in the claim chat shows; acceptable for + now (iOS has the same hole past the card check). + +## Out of scope + +- The experimental AI development flow's step-loop issue (backend-side, tracked in + #rnd-claims-automation). +- Deep links directly into a resumed claim. +- Bootstrap entry for the new flag (off-when-unfetched is the desired default). From 94ff4fd25f73c5a8af7c7140cf31f5ce289367b0 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 17:24:39 +0200 Subject: [PATCH 10/28] Add resume-draft-claim implementation plan --- .../plans/2026-07-03-resume-draft-claim.md | 1634 +++++++++++++++++ 1 file changed, 1634 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-03-resume-draft-claim.md diff --git a/docs/superpowers/plans/2026-07-03-resume-draft-claim.md b/docs/superpowers/plans/2026-07-03-resume-draft-claim.md new file mode 100644 index 0000000000..8b563dbcfe --- /dev/null +++ b/docs/superpowers/plans/2026-07-03-resume-draft-claim.md @@ -0,0 +1,1634 @@ +# Resume Draft Claim Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Bring the Android `feat-resume-claim` branch to parity with iOS PR HedvigInsurance/ugglan#2434: draft card on home, draft/delete/expired/leave dialogs, `enable_claim_intent_resume` flag gating, displayName chat title. + +**Architecture:** A new KMP `data-claim-intent` module owns the shared resumable-draft queries (get + delete) behind project-owned types. Home carries the draft in its existing Home query (flag-gated via `@include`), the claim-cards pager gains a sealed card type with a new `DraftClaimCard`, and both start-claim entry points (home + inbox) show a `DraftClaimDialog` before the pledge sheet when a draft exists. The claim chat tracks `displayName`/`resumable` through its single `handleNext` seam. + +**Tech Stack:** Kotlin/KMP, Jetpack Compose, Apollo GraphQL (octopus), Metro DI, Molecule, Navigation 3, Unleash feature flags, Lokalise compose resources. + +**Spec:** `docs/superpowers/specs/2026-07-03-resume-draft-claim-design.md` (approved). Read it for the behavioral rationale; this plan is the how. + +## Global Constraints + +- Working branch: `feat-resume-claim`. Repo root: `/Users/stylianosgakis/hedvig/apps/android_copy_2`. Run all gradle commands from the repo root. +- Unleash flag key is exactly `enable_claim_intent_resume`; enum entry `ENABLE_CLAIM_INTENT_RESUME`. No bootstrap entry. +- All user-facing copy uses the `RESUME_CLAIM_*` Lokalise keys (verified in Task 1). Exception: the "Started" progress-segment label, which has no Lokalise key; hardcode it with a `// TODO: Add "Started" / "Påbörjad" to Lokalise` comment (iOS hardcodes it too). +- Never call Timber/Log/println; use `logcat` from `:logging-public`. +- GraphQL `octopus.*` types must not leak into public signatures; map to project-owned types inside `internal` impls. +- Kotlin style: 2-space indent, trailing commas, no wildcard imports, max line 120. Run `./gradlew ktlintFormat` before every commit. +- Never use " — " (spaced em-dash) in any prose you write (commit messages, comments): use commas, colons, or parentheses. +- `ClaimIntent.createdAt` is the `DateTime` scalar, mapped to `kotlin.time.Instant` (see `app/apollo/apollo-octopus-public/build.gradle.kts:29`). All `startedAt` fields in this plan are `kotlin.time.Instant`. +- Feature modules must not depend on other feature modules. The new shared code goes in `app/data/data-claim-intent` and `app/ui/claim-status` and `app/design-system/design-system-hedvig`. +- Commit after every task. Commit messages are short imperative sentences without a `feat:` prefix (match `git log` style, e.g. "Add resume-draft-claim design spec"). + +--- + +### Task 1: Groundwork: commit schema, download and verify strings + +**Files:** +- Modify (commit only): `app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls` (already changed on disk, unstaged) +- Modify (generated): `app/core/core-resources/src/commonMain/composeResources/values*/strings.xml` (via `downloadStrings`) + +**Interfaces:** +- Produces: schema fields `ClaimIntent.resumable: Boolean!`, `Mutation.claimIntentDeleteDraft(id: ID!): Boolean!`, and string resources `Res.string.RESUME_CLAIM_*` used by every later task. + +- [ ] **Step 1: Verify and commit the schema diff** + +Run: `git -C /Users/stylianosgakis/hedvig/apps/android_copy_2 diff --stat -- app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls` +Expected: 1 file changed (the diff adds `resumable: Boolean!` on `ClaimIntent` and the `claimIntentDeleteDraft` mutation). + +```bash +cd /Users/stylianosgakis/hedvig/apps/android_copy_2 +git add app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +git commit -m "Download schema" +``` + +- [ ] **Step 2: Download strings** + +Run: `./gradlew downloadStrings` +Expected: BUILD SUCCESSFUL; `strings.xml` files regenerate. (Requires `lokalise.properties`; if the task fails on credentials, stop and ask the user.) + +- [ ] **Step 3: Verify the RESUME_CLAIM keys arrived** + +Run: `rg -c "RESUME_CLAIM" app/core/core-resources/src/commonMain/composeResources/values/strings.xml` +Expected: a count >= 17 (keys: CONTINUE_BUTTON, DEFAULT_TITLE, DELETE_BODY, DELETE_BUTTON, DELETE_TITLE, DRAFT, DRAFT_ALERT_BODY, DRAFT_ALERT_CONTINUE, DRAFT_ALERT_START_NEW, DRAFT_ALERT_TITLE, EXPIRED_BODY, EXPIRED_TITLE, FALLBACK_TITLE, LEAVE_BODY, LEAVE_CONFIRM, LEAVE_TITLE, STATED). +If any key is missing, STOP and report to the user; do not hardcode substitutes. + +Note: `RESUME_CLAIM_STATED` is a format string ("Started %1$s" in the Android export); it takes one argument. + +- [ ] **Step 4: Commit the strings** + +```bash +git add -A app/core/core-resources +git commit -m "Download strings with RESUME_CLAIM keys" +``` + +--- + +### Task 2: Feature flag ENABLE_CLAIM_INTENT_RESUME + +**Files:** +- Modify: `app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt` +- Modify: `app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt` + +**Interfaces:** +- Produces: `Feature.ENABLE_CLAIM_INTENT_RESUME` enum entry, read everywhere later via `featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME): Flow`. + +- [ ] **Step 1: Add the enum entry** + +In `Feature.kt`, add to the `Feature` enum (alphabetical placement next to the existing entries is fine; match the existing style): + +```kotlin +ENABLE_CLAIM_INTENT_RESUME( + "Enables resuming a draft claim: the draft card on the home screen, the draft-claim dialogs, " + + "and the resumable-aware leave dialog in the claim chat.", +), +``` + +- [ ] **Step 2: Map the Unleash key** + +In `FeatureUnleashKey.kt`, add to the `when`: + +```kotlin +Feature.ENABLE_CLAIM_INTENT_RESUME -> "enable_claim_intent_resume" +``` + +- [ ] **Step 3: Compile (an exhaustive `when` elsewhere may need the new entry)** + +Run: `./gradlew :feature-flags:compileDebugKotlinAndroid` +Expected: BUILD SUCCESSFUL. If any other module fails later on an exhaustive `when` over `Feature`, add the entry there following the file's existing pattern. + +No bootstrap change: the flag must resolve to off when Unleash was never fetched, which is the desired default (see `app/featureflags/feature-flags/FEATURE_FLAG_DEFAULTS.md`). + +- [ ] **Step 4: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/featureflags +git commit -m "Add enable_claim_intent_resume feature flag" +``` + +--- + +### Task 3: New data module data-claim-intent + +**Files:** +- Create: `app/data/data-claim-intent/build.gradle.kts` +- Create: `app/data/data-claim-intent/src/commonMain/graphql/QueryResumableClaimIntent.graphql` +- Create: `app/data/data-claim-intent/src/commonMain/graphql/MutationClaimIntentDeleteDraft.graphql` +- Create: `app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/ResumableClaimIntent.kt` +- Create: `app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/GetResumableClaimIntentUseCase.kt` +- Create: `app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/DeleteClaimIntentDraftUseCase.kt` + +**Interfaces:** +- Produces (public API consumed by Tasks 8, 9, 11): + +```kotlin +data class ResumableClaimIntent(val id: String, val displayName: String?, val startedAt: Instant) +interface GetResumableClaimIntentUseCase { suspend fun invoke(): Either } +interface DeleteClaimIntentDraftUseCase { suspend fun invoke(id: String): Either } +``` + +Both bound in Metro `AppScope` via `@ContributesBinding`. Type-safe accessor: `projects.dataClaimIntent`. + +The module is modeled on `app/data/data-conversations` (KMP, apollo, no test source set; the mapping is trivial and consumer behavior is tested in HomePresenterTest in Task 8). + +- [ ] **Step 1: Create the build file** + +`app/data/data-claim-intent/build.gradle.kts`: + +```kotlin +plugins { + id("hedvig.multiplatform.library") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.arrow.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.loggingPublic) + } + } +} +``` + +The module is auto-discovered by `settings.gradle.kts` (any directory under `app/` with a `build.gradle.kts`). + +- [ ] **Step 2: Create the GraphQL operations** + +`QueryResumableClaimIntent.graphql`: + +```graphql +query ResumableClaimIntent { + currentMember { + resumableClaimIntent { + id + displayName + createdAt + } + } +} +``` + +`MutationClaimIntentDeleteDraft.graphql`: + +```graphql +mutation ClaimIntentDeleteDraft($id: ID!) { + claimIntentDeleteDraft(id: $id) +} +``` + +- [ ] **Step 3: Create the model** + +`ResumableClaimIntent.kt`: + +```kotlin +package com.hedvig.android.data.claimintent + +import kotlin.time.Instant + +data class ResumableClaimIntent( + val id: String, + val displayName: String?, + val startedAt: Instant, +) +``` + +- [ ] **Step 4: Create the get use case** + +`GetResumableClaimIntentUseCase.kt`: + +```kotlin +package com.hedvig.android.data.claimintent + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.logger.logcat +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import octopus.ResumableClaimIntentQuery + +interface GetResumableClaimIntentUseCase { + /** + * A null right side means the member has no resumable draft claim. + */ + suspend fun invoke(): Either +} + +@Inject +@ContributesBinding(AppScope::class) +internal class GetResumableClaimIntentUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetResumableClaimIntentUseCase { + override suspend fun invoke(): Either { + return either { + apolloClient + .query(ResumableClaimIntentQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .mapLeft { error -> + logcat(operationError = error) { "GetResumableClaimIntentUseCase failed with $error" } + ErrorMessage() + } + .bind() + .currentMember + .resumableClaimIntent + ?.let { resumableClaimIntent -> + ResumableClaimIntent( + id = resumableClaimIntent.id, + displayName = resumableClaimIntent.displayName, + startedAt = resumableClaimIntent.createdAt, + ) + } + } + } +} +``` + +Note: the `logcat(operationError = ...)` overload lives next to `safeExecute` in `:apollo-core` (`com.hedvig.android.apollo`); copy the import that `GetHomeDataUseCase.kt` uses for it. If the memory-cache dependency is missing for `fetchPolicy`, add `implementation(libs.apollo.normalizedCache)` to `commonMain.dependencies`. + +- [ ] **Step 5: Create the delete use case** + +`DeleteClaimIntentDraftUseCase.kt`: + +```kotlin +package com.hedvig.android.data.claimintent + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.logger.logcat +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import octopus.ClaimIntentDeleteDraftMutation + +interface DeleteClaimIntentDraftUseCase { + suspend fun invoke(id: String): Either +} + +@Inject +@ContributesBinding(AppScope::class) +internal class DeleteClaimIntentDraftUseCaseImpl( + private val apolloClient: ApolloClient, +) : DeleteClaimIntentDraftUseCase { + override suspend fun invoke(id: String): Either { + return either { + val deleted = apolloClient + .mutation(ClaimIntentDeleteDraftMutation(id)) + .safeExecute() + .mapLeft { error -> + logcat(operationError = error) { "DeleteClaimIntentDraftUseCase failed with $error" } + ErrorMessage() + } + .bind() + .claimIntentDeleteDraft + ensure(deleted) { ErrorMessage() } + } + } +} +``` + +- [ ] **Step 6: Compile** + +Run: `./gradlew :data-claim-intent:compileKotlinJvm` +Expected: BUILD SUCCESSFUL (Apollo codegen runs first). If the KMP target set differs from data-conversations and `compileKotlinJvm` doesn't exist, run `./gradlew :data-claim-intent:tasks --all | rg compile` and use the main compile task. + +- [ ] **Step 7: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/data/data-claim-intent +git commit -m "Add data-claim-intent module with resumable draft get and delete use cases" +``` + +--- + +### Task 4: Revert StartClaimBottomSheet, add DraftClaimDialog + +**Files:** +- Modify (revert to develop): `app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt` +- Create: `app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/DraftClaimDialog.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt` (sheet call site, ~lines 230-300) +- Modify: `app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt` (sheet call site, ~lines 130-160) + +**Interfaces:** +- Consumes: `Res.string.RESUME_CLAIM_DRAFT_ALERT_*` (Task 1). +- Produces: `StartClaimBottomSheet(state: HedvigBottomSheetState, navigateToClaimChat: () -> Unit)` (back to the develop signature; `StartClaimSheetData` is deleted) and: + +```kotlin +@Composable +fun DraftClaimDialog( + onDismissRequest: () -> Unit, + onContinueDraft: () -> Unit, + onStartNewClaim: () -> Unit, + modifier: Modifier = Modifier, +) +``` + +- [ ] **Step 1: Revert the sheet file to the develop version** + +The develop version is exactly the pre-branch state we want (sheet keyed on `Unit`, no draft button, no `StartClaimSheetData`, no stray formatting): + +```bash +git checkout develop -- app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt +``` + +- [ ] **Step 2: Create DraftClaimDialog** + +`DraftClaimDialog.kt`: + +```kotlin +package com.hedvig.android.design.system.hedvig + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_BODY +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_CONTINUE +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_START_NEW +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_TITLE +import hedvig.resources.Res +import hedvig.resources.general_cancel_button +import org.jetbrains.compose.resources.stringResource + +@Composable +fun DraftClaimDialog( + onDismissRequest: () -> Unit, + onContinueDraft: () -> Unit, + onStartNewClaim: () -> Unit, + modifier: Modifier = Modifier, +) { + HedvigDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + ) { + Column { + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_TITLE), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_BODY), + textAlign = TextAlign.Center, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(24.dp)) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_CONTINUE), + onClick = onContinueDraft, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_START_NEW), + onClick = onStartNewClaim, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Red, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = stringResource(Res.string.general_cancel_button), + onClick = onDismissRequest, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@HedvigPreview +@Composable +private fun PreviewDraftClaimDialog() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + DraftClaimDialog({}, {}, {}) + } + } +} +``` + +If `HedvigDialog`'s default `style` (`DialogDefaults.defaultDialogStyle`) is not `NoButtons`, pass `style = DialogDefaults.DialogStyle.NoButtons` explicitly (check `Dialog.kt:287` and the `defaultDialogStyle` value). + +- [ ] **Step 3: Fix the home call site** + +In `HomeDestination.kt`, the current block (~line 230): + +```kotlin +val resumableClaimId = (uiState as? Success)?.resumableClaimId +val startClaimBottomSheetState = rememberHedvigBottomSheetState() +StartClaimBottomSheet( + state = startClaimBottomSheetState, + navigateToOldClaim = { + navigateToClaimChat(resumableClaimId) + }, + navigateToClaimChat = { + navigateToClaimChat(null) + }, +) +``` + +becomes: + +```kotlin +val startClaimBottomSheetState = rememberHedvigBottomSheetState() +StartClaimBottomSheet( + state = startClaimBottomSheetState, + navigateToClaimChat = { + navigateToClaimChat(null) + }, +) +``` + +and further down, `openClaimFlowSheet = { startClaimBottomSheetState.show(StartClaimSheetData(resumableClaimId)) }` becomes `openClaimFlowSheet = { startClaimBottomSheetState.show(Unit) }`. Remove the now-unused `StartClaimSheetData` import. (`navigateToClaimChat` is still `(String?) -> Unit` at this point; Task 6 changes it to `(Boolean) -> Unit`. The draft dialog gets wired in Task 9.) + +- [ ] **Step 4: Fix the inbox call site** + +In `InboxDestination.kt` (~line 132): change `rememberHedvigBottomSheetState()` back to `rememberHedvigBottomSheetState()`, `startClaimBottomSheetState.show(StartClaimSheetData(null))` back to `.show(Unit)`, and delete the `navigateToOldClaim = {}` argument and the `StartClaimSheetData` import. + +- [ ] **Step 5: Compile all three modules** + +Run: `./gradlew :design-system-hedvig:compileDebugKotlinAndroid :feature-home:compileDebugKotlin :feature-chat:compileDebugKotlin` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 6: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/design-system app/feature/feature-home app/feature/feature-chat +git commit -m "Revert draft entry from pledge sheet and add DraftClaimDialog" +``` + +--- + +### Task 5: Draft card in ui/claim-status + +**Files:** +- Create: `app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimCardUiState.kt` +- Create: `app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/DraftClaimCard.kt` +- Modify: `app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/ClaimStatusCards.kt` +- Modify: `app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt` (add `Started` segment text) +- Modify: `app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/internal/ClaimProgressRow.kt` (map `Started`) +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt` (the `claimStatusCards = { ... }` block, ~line 485) + +**Interfaces:** +- Consumes: `Res.string.RESUME_CLAIM_DRAFT`, `RESUME_CLAIM_FALLBACK_TITLE`, `RESUME_CLAIM_STATED`, `RESUME_CLAIM_DELETE_BUTTON`, `RESUME_CLAIM_CONTINUE_BUTTON` (Task 1). +- Produces (consumed by Task 9): + +```kotlin +sealed interface ClaimCardUiState { + data class Claim(val uiState: ClaimStatusCardUiState) : ClaimCardUiState + data class Draft(val id: String, val title: String?, val startedAt: Instant) : ClaimCardUiState +} + +@Composable +fun ClaimStatusCards( + onClick: (claimId: String) -> Unit, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (draftId: String) -> Unit, + claimCardsUiState: NonEmptyList, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) +``` + +- [ ] **Step 1: Add the sealed card type** + +`model/ClaimCardUiState.kt`: + +```kotlin +package com.hedvig.android.ui.claimstatus.model + +import kotlin.time.Instant + +sealed interface ClaimCardUiState { + data class Claim(val uiState: ClaimStatusCardUiState) : ClaimCardUiState + + data class Draft( + val id: String, + val title: String?, + val startedAt: Instant, + ) : ClaimCardUiState +} +``` + +- [ ] **Step 2: Add the Started segment label** + +In `model/ClaimProgressSegment.kt` change the enum: + +```kotlin +enum class SegmentText { + Submitted, + BeingHandled, + Closed, + Started, +} +``` + +In `internal/ClaimProgressRow.kt`, the `when (segmentText)` at line ~80 gains: + +```kotlin +// TODO: Add "Started" / "Påbörjad" to Lokalise +Started -> "Started" +``` + +(add the `Started` import next to the existing `Submitted`/`BeingHandled`/`Closed` imports). + +- [ ] **Step 3: Create DraftClaimCard** + +`DraftClaimCard.kt`: + +```kotlin +package com.hedvig.android.ui.claimstatus + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Medium +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Primary +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Secondary +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigDateTimeFormatterDefaults +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighLightSize +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.MEDIUM +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.datepicker.getLocale +import com.hedvig.android.ui.claimstatus.internal.ClaimProgressRow +import com.hedvig.android.ui.claimstatus.model.ClaimCardUiState +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType.INACTIVE +import hedvig.resources.RESUME_CLAIM_CONTINUE_BUTTON +import hedvig.resources.RESUME_CLAIM_DELETE_BUTTON +import hedvig.resources.RESUME_CLAIM_DRAFT +import hedvig.resources.RESUME_CLAIM_FALLBACK_TITLE +import hedvig.resources.RESUME_CLAIM_STATED +import hedvig.resources.Res +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.compose.resources.stringResource + +@Composable +fun DraftClaimCard( + uiState: ClaimCardUiState.Draft, + onContinueClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + HedvigCard(modifier = modifier) { + Column(Modifier.padding(16.dp)) { + HighlightLabel( + labelText = stringResource(Res.string.RESUME_CLAIM_DRAFT), + size = HighLightSize.Small, + color = HighlightColor.Amber(MEDIUM), + ) + Spacer(Modifier.height(16.dp)) + HedvigText( + text = uiState.title ?: stringResource(Res.string.RESUME_CLAIM_FALLBACK_TITLE), + style = HedvigTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 2.dp), + ) + val formattedDate = HedvigDateTimeFormatterDefaults + .dateMonthAndYear(getLocale()) + .format(uiState.startedAt.toLocalDateTime(TimeZone.currentSystemDefault())) + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_STATED, formattedDate), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.padding(horizontal = 2.dp), + ) + Spacer(Modifier.height(18.dp)) + ClaimProgressRow( + claimProgressItemsUiState = listOf( + ClaimProgressSegment(SegmentText.Started, INACTIVE), + ClaimProgressSegment(SegmentText.BeingHandled, INACTIVE), + ClaimProgressSegment(SegmentText.Closed, INACTIVE), + ), + ) + Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), + onClick = onDeleteClick, + enabled = true, + buttonStyle = Secondary, + buttonSize = Medium, + modifier = Modifier.weight(1f), + ) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_CONTINUE_BUTTON), + onClick = onContinueClick, + enabled = true, + buttonStyle = Primary, + buttonSize = Medium, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@HedvigPreview +@Composable +private fun PreviewDraftClaimCard() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + DraftClaimCard( + uiState = ClaimCardUiState.Draft( + id = "id", + title = "My things", + startedAt = Instant.parse("2026-07-02T00:00:00Z"), + ), + onContinueClick = {}, + onDeleteClick = {}, + ) + } + } +} +``` + +Verify the exact nesting of `HighLightSize`/`HighlightColor`/`HighlightShade` in `HighlightLabel.kt` (they may be top-level in the file rather than inside `HighlightLabelDefaults`); adjust imports to match, keeping `Amber` + medium shade + small size. + +- [ ] **Step 4: Rework ClaimStatusCards over the sealed type** + +Replace the two branches in `ClaimStatusCards.kt` so both the single-card and pager paths render through one private helper: + +```kotlin +@Composable +fun ClaimStatusCards( + onClick: (claimId: String) -> Unit, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (draftId: String) -> Unit, + claimCardsUiState: NonEmptyList, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + if (claimCardsUiState.size == 1) { + ClaimCard( + uiState = claimCardsUiState.first(), + onClick = onClick, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, + modifier = modifier.padding(contentPadding), + ) + } else { + val pagerState = rememberPagerState(pageCount = { claimCardsUiState.size }) + Column(modifier) { + HorizontalPager( + state = pagerState, + contentPadding = contentPadding, + beyondViewportPageCount = 1, + pageSpacing = 8.dp, + modifier = Modifier.fillMaxWidth().systemGestureExclusion(), + ) { page: Int -> + ClaimCard( + uiState = claimCardsUiState[page], + onClick = onClick, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, + modifier = Modifier.fillMaxWidth(), + ) + } + Spacer(Modifier.height(16.dp)) + HorizontalPagerIndicator( + pagerState = pagerState, + pageCount = claimCardsUiState.size, + activeColor = LocalContentColor.current, + modifier = Modifier.padding(contentPadding).align(Alignment.CenterHorizontally), + ) + } + } +} + +@Composable +private fun ClaimCard( + uiState: ClaimCardUiState, + onClick: (claimId: String) -> Unit, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (draftId: String) -> Unit, + modifier: Modifier = Modifier, +) { + when (uiState) { + is ClaimCardUiState.Claim -> ClaimStatusCard( + uiState = uiState.uiState, + onClick = onClick, + modifier = modifier, + ) + + is ClaimCardUiState.Draft -> DraftClaimCard( + uiState = uiState, + onContinueClick = onContinueDraftClaim, + onDeleteClick = { onDeleteDraftClaim(uiState.id) }, + modifier = modifier, + ) + } +} +``` + +Update the file's preview to wrap its fake states in `ClaimCardUiState.Claim(...)` and add one `ClaimCardUiState.Draft("id", "My things", Instant.parse("2026-07-02T00:00:00Z"))` first in the list. + +- [ ] **Step 5: Update the only external caller (feature-home)** + +Run: `rg -n "ClaimStatusCards\(" app/ --type kotlin -g '!*claim-status*'` +Expected: only `HomeDestination.kt`. In its `claimStatusCards = { ... }` block (~line 485), adapt (draft callbacks stay no-ops until Task 9): + +```kotlin +claimStatusCards = { + val claimCards = uiState.claimStatusCardsData?.claimStatusCardsUiState + ?.map { ClaimCardUiState.Claim(it) } + ?.toNonEmptyListOrNull() + if (claimCards != null) { + ClaimStatusCards( + onClick = onClaimDetailCardClicked, + onContinueDraftClaim = {}, + onDeleteDraftClaim = {}, + claimCardsUiState = claimCards, + contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets, + ) + } +}, +``` + +with imports `com.hedvig.android.ui.claimstatus.model.ClaimCardUiState` and `arrow.core.toNonEmptyListOrNull`. + +- [ ] **Step 6: Compile** + +Run: `./gradlew :claim-status:compileDebugKotlin :feature-home:compileDebugKotlin` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 7: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/ui/claim-status app/feature/feature-home +git commit -m "Add draft claim card to the claim status cards pager" +``` + +--- + +### Task 6: ClaimChatKey.resumeClaim and navigation plumbing + +**Files:** +- Modify: `app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt` +- Modify: `app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt` +- Modify: `app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/navigation/CbmChatEntries.kt` +- Modify: `app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt` + +**Interfaces:** +- Produces: `ClaimChatKey(isDevelopmentFlow: Boolean = false, messageId: String? = null, resumeClaim: Boolean = false)`; all `navigateToClaimChat` lambdas become `(resumeClaim: Boolean) -> Unit`; `ClaimChatViewModelFactory.create(developmentFlow: Boolean, resumeClaim: Boolean)`. +- Rationale: the resume query takes no id (one draft per member; iOS passes no id), and a stale id after process-death restore would be misleading. + +- [ ] **Step 1: Change the key** + +In `ClaimChatEntries.kt`: + +```kotlin +@Serializable +data class ClaimChatKey( + val isDevelopmentFlow: Boolean = false, + val messageId: String? = null, + val resumeClaim: Boolean = false, +) : HedvigNavKey +``` + +and in the `entry` block: `resumableClaimId = key.resumableClaimId,` becomes `resumeClaim = key.resumeClaim,`. + +- [ ] **Step 2: Change the ViewModel's assisted params** + +Two same-typed `@Assisted` Booleans need identifiers (the `:viewmodel-processor` propagates them; see `HedvigViewModelProcessorTest`, the `@Assisted("topicId")` test): + +```kotlin +@AssistedInject +@HedvigViewModel(ActivityRetainedScope::class) +internal class ClaimChatViewModel( + @Assisted("developmentFlow") developmentFlow: Boolean, + @Assisted("resumeClaim") resumeClaim: Boolean, + startClaimIntentUseCase: StartClaimIntentUseCase, + // ... rest unchanged ... +``` + +Thread it to the presenter: `ClaimChatPresenter(..., resumeClaim, resumeClaimUseCase)`; in `ClaimChatPresenter` replace `private val resumableClaimId: String?` with `private val resumeClaim: Boolean`, and in the initializing block replace `val isResumingClaim = resumableClaimId != null` with `val isResumingClaim = resumeClaim`. + +- [ ] **Step 3: Change the destination** + +In `ClaimChatDestination.kt`: parameter `resumableClaimId: String?` becomes `resumeClaim: Boolean`, and the resolution becomes `create(isDevelopmentFlow, resumeClaim)`. + +- [ ] **Step 4: Update all navigate lambdas** + +- `HedvigEntryProvider.kt` `addHomeEntries` wiring: + +```kotlin +navigateToClaimChat = { resumeClaim -> + backstack.add( + ClaimChatKey( + messageId = null, + isDevelopmentFlow = false, + resumeClaim = resumeClaim, + ), + ) +}, +``` + +- `HedvigEntryProvider.kt` `addChatEntries` wiring: + +```kotlin +navigateToClaimChat = { resumeClaim -> + backstack.add(ClaimChatKey(messageId = null, isDevelopmentFlow = false, resumeClaim = resumeClaim)) +}, +``` + +- `HomeEntries.kt`: `navigateToClaimChat: (String?) -> Unit` becomes `navigateToClaimChat: (resumeClaim: Boolean) -> Unit`. +- `HomeDestination.kt`: both `navigateToClaimChat: (String?) -> Unit` params become `(resumeClaim: Boolean) -> Unit`; the sheet callback becomes `navigateToClaimChat = { navigateToClaimChat(false) }`. +- `CbmChatEntries.kt`: `navigateToClaimChat: () -> Unit` becomes `navigateToClaimChat: (resumeClaim: Boolean) -> Unit` (passed through to `InboxDestination` unchanged otherwise). +- `InboxDestination.kt`: `navigateToClaimChat: () -> Unit` params become `(resumeClaim: Boolean) -> Unit`; the sheet callback body becomes `navigateToClaimChat(false)`. + +Run `rg -n "resumableClaimId" app/` afterwards; the only remaining hits must be inside `feature-claim-chat`'s presenter history (none expected) or nothing at all except `feature-home`'s data layer (`GetHomeDataUseCase.kt`, `HomePresenter.kt`, `HomePresenterTest.kt`), which Task 7 removes. + +- [ ] **Step 5: Compile and run the serialization guard** + +Run: `./gradlew :feature-claim-chat:compileDebugKotlinAndroid :feature-home:compileDebugKotlin :feature-chat:compileDebugKotlin :app:compileDevelopKotlin` +Expected: BUILD SUCCESSFUL. (If `:app:compileDevelopKotlin` is not a task, use `./gradlew :app:assembleDevelop -x lint`.) + +Run: `./gradlew :app:test --tests "*ExhaustiveBackStackSerializationTest*"` +Expected: PASS (the key is still `@Serializable` with defaults; `navKeys()` is already applied in feature-claim-chat). + +- [ ] **Step 6: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/ +git commit -m "Replace ClaimChatKey resumableClaimId with resumeClaim flag" +``` + +--- + +### Task 7: Home data layer: DraftClaim in the Home query, flag-gated + +**Files:** +- Modify: `app/feature/feature-home/src/main/graphql/QueryHome.graphql` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt` +- Test: `app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt` + +**Interfaces:** +- Consumes: `Feature.ENABLE_CLAIM_INTENT_RESUME` (Task 2). +- Produces (consumed by Tasks 8, 9): + +```kotlin +// in HomeData: +val draftClaim: DraftClaim? // replaces resumableClaimId: String? +data class DraftClaim(val id: String, val displayName: String?, val startedAt: Instant) { + fun isExpired(now: Instant): Boolean +} +// in HomeUiState.Success: +val draftClaim: HomeData.DraftClaim? // replaces resumableClaimId: String? +``` + +- [ ] **Step 1: Extend the query, flag-gated** + +`QueryHome.graphql` header and the resumable selection become: + +```graphql +query Home($claimsHistoryFlag: Boolean!, $resumeClaimEnabled: Boolean!) { + currentMember { + resumableClaimIntent @include(if: $resumeClaimEnabled) { + id + displayName + createdAt + } +``` + +(rest of the query unchanged). + +- [ ] **Step 2: Read the flag before building the query** + +In `GetHomeDataUseCase.kt`, wrap the existing `combine(...)` in a `flatMapLatest` on the flag so the query variable is available (the existing `featureManager` property is already injected): + +```kotlin +@OptIn(ExperimentalCoroutinesApi::class) +override fun invoke(forceNetworkFetch: Boolean): Flow> { + return featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME) + .flatMapLatest { resumeClaimEnabled -> + combine( + apolloClient.query(HomeQuery(true, resumeClaimEnabled)) + .fetchPolicy(if (forceNetworkFetch) FetchPolicy.NetworkOnly else FetchPolicy.CacheAndNetwork) + .safeFlow(), + // ... the five other flows, unchanged ... + ) { + // ... existing lambda, unchanged except the HomeData construction below ... + } + } +} +``` + +Add imports `kotlinx.coroutines.ExperimentalCoroutinesApi` and `kotlinx.coroutines.flow.flatMapLatest`. In the `HomeData(...)` construction, replace `resumableClaimId = homeQueryData.currentMember.resumableClaimIntent?.id` with: + +```kotlin +draftClaim = homeQueryData.currentMember.resumableClaimIntent?.let { resumableClaimIntent -> + HomeData.DraftClaim( + id = resumableClaimIntent.id, + displayName = resumableClaimIntent.displayName, + startedAt = resumableClaimIntent.createdAt, + ) +}, +``` + +- [ ] **Step 3: Replace the HomeData field** + +In the `HomeData` data class, replace `val resumableClaimId: String?` with `val draftClaim: DraftClaim?` and add inside `HomeData`: + +```kotlin +data class DraftClaim( + val id: String, + val displayName: String?, + val startedAt: Instant, +) { + /** + * Drafts are kept for 7 days on the backend ("Your claim is automatically saved for 7 days"). + * Client-side heuristic, same as iOS. + */ + fun isExpired(now: Instant): Boolean = now > startedAt + 7.days +} +``` + +Imports: `kotlin.time.Instant`, `kotlin.time.Duration.Companion.days`. + +In `GetHomeDataUseCaseDemo.kt`: `resumableClaimId = null` becomes `draftClaim = null`. + +- [ ] **Step 4: Thread through the presenter** + +In `HomePresenter.kt`, in `HomeUiState.Success`, `SuccessData`, and both mapping spots (`fromLastState`, the `fromHomeData`-style builder), replace `resumableClaimId: String?` / `resumableClaimId = ...` with `draftClaim: HomeData.DraftClaim?` / `draftClaim = ...` (the value comes from `homeData.draftClaim` and `lastState.draftClaim`). + +- [ ] **Step 5: Fix the tests mechanically** + +In `HomePresenterTest.kt`, replace every `resumableClaimId = null` with `draftClaim = null` (17 occurrences per the branch diff; use find-replace). + +- [ ] **Step 6: Run the tests** + +Run: `./gradlew :feature-home:test` +Expected: PASS. + +- [ ] **Step 7: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/feature/feature-home +git commit -m "Carry flag-gated DraftClaim through the home data layer" +``` + +--- + +### Task 8: HomePresenter delete-draft event (TDD) + +**Files:** +- Modify: `app/feature/feature-home/build.gradle.kts` (add `implementation(projects.dataClaimIntent)`) +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt` +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt` +- Test: `app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt` + +**Interfaces:** +- Consumes: `DeleteClaimIntentDraftUseCase` (Task 3), `HomeData.DraftClaim` (Task 7). +- Produces: `HomeEvent.DeleteDraftClaim(val draftId: String)`; `HomePresenter` constructor gains a trailing `deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase` param (consumed by Task 9's UI wiring). + +- [ ] **Step 1: Add the module dependency** + +In `app/feature/feature-home/build.gradle.kts` dependencies block (alphabetical): `implementation(projects.dataClaimIntent)`. + +- [ ] **Step 2: Write the failing tests** + +Add to `HomePresenterTest.kt` (reuse the existing `TestGetHomeDataUseCase`, `someIrrelevantHomeDataInstance`, `homePresenter.test` pattern; every existing `HomePresenter(...)` construction in the file also gains the new last argument `TestDeleteClaimIntentDraftUseCase()`): + +```kotlin +@Test +fun `deleting the draft claim calls the use case and reloads home on success`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val deleteClaimIntentDraftUseCase = TestDeleteClaimIntentDraftUseCase() + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + deleteClaimIntentDraftUseCase, + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isFalse() + getHomeDataUseCase.responseTurbine.add( + someIrrelevantHomeDataInstance.copy( + draftClaim = HomeData.DraftClaim("draft-id", "My things", Instant.parse("2026-07-01T00:00:00Z")), + ).right(), + ) + assertThat(awaitItem()).isInstanceOf() + + sendEvent(HomeEvent.DeleteDraftClaim("draft-id")) + assertThat(deleteClaimIntentDraftUseCase.deletedIdsTurbine.awaitItem()).isEqualTo("draft-id") + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isTrue() + } +} + +@Test +fun `a failed draft deletion does not reload home`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val deleteClaimIntentDraftUseCase = TestDeleteClaimIntentDraftUseCase().apply { + result = ErrorMessage().left() + } + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + deleteClaimIntentDraftUseCase, + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isFalse() + getHomeDataUseCase.responseTurbine.add(someIrrelevantHomeDataInstance.right()) + assertThat(awaitItem()).isInstanceOf() + + sendEvent(HomeEvent.DeleteDraftClaim("draft-id")) + assertThat(deleteClaimIntentDraftUseCase.deletedIdsTurbine.awaitItem()).isEqualTo("draft-id") + getHomeDataUseCase.forceNetworkFetchTurbine.expectNoEvents() + } +} +``` + +and the fake at the bottom of the file: + +```kotlin +private class TestDeleteClaimIntentDraftUseCase : DeleteClaimIntentDraftUseCase { + val deletedIdsTurbine = Turbine() + var result: Either = Unit.right() + + override suspend fun invoke(id: String): Either { + deletedIdsTurbine.add(id) + return result + } +} +``` + +Imports needed: `com.hedvig.android.data.claimintent.DeleteClaimIntentDraftUseCase`, `com.hedvig.android.core.common.ErrorMessage`, `kotlin.time.Instant`, `arrow.core.left`. + +- [ ] **Step 3: Run the tests to verify they fail** + +Run: `./gradlew :feature-home:test --tests "*HomePresenterTest*"` +Expected: FAIL to compile ("no value passed for parameter" / unresolved `DeleteDraftClaim`). + +- [ ] **Step 4: Implement** + +`HomePresenter.kt`: + +```kotlin +internal class HomePresenter( + private val getHomeDataUseCase: GetHomeDataUseCase, + private val seenImportantMessagesStorage: SeenImportantMessagesStorage, + private val crossSellHomeNotificationService: CrossSellHomeNotificationService, + private val applicationScope: ApplicationScope, + private val isProduction: Boolean, + private val deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase, +) : MoleculePresenter { +``` + +New event in `HomeEvent`: + +```kotlin +data class DeleteDraftClaim(val draftId: String) : HomeEvent +``` + +In the `CollectEvents` block: + +```kotlin +is HomeEvent.DeleteDraftClaim -> { + launch { + deleteClaimIntentDraftUseCase.invoke(homeEvent.draftId).fold( + ifLeft = { logcat(LogPriority.ERROR) { "Failed to delete draft claim: $it" } }, + ifRight = { loadIteration++ }, + ) + } +} +``` + +`HomeViewModel.kt`: add `deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase` to the constructor and pass it as the presenter's last argument (Metro resolves it; no other DI wiring needed). + +- [ ] **Step 5: Run the tests to verify they pass** + +Run: `./gradlew :feature-home:test` +Expected: PASS (all of HomePresenterTest, including the two new tests). + +- [ ] **Step 6: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/feature/feature-home +git commit -m "Add delete-draft-claim event to the home presenter" +``` + +--- + +### Task 9: Home UI wiring: draft card, dialogs, draft-aware start flow + +**Files:** +- Modify: `app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt` + +**Interfaces:** +- Consumes: `ClaimCardUiState`/`ClaimStatusCards`/`DraftClaimCard` (Task 5), `DraftClaimDialog` (Task 4), `HomeUiState.Success.draftClaim` + `DraftClaim.isExpired` (Task 7), `HomeEvent.DeleteDraftClaim` (Task 8), `navigateToClaimChat: (Boolean) -> Unit` (Task 6), `Res.string.RESUME_CLAIM_DELETE_*` and `RESUME_CLAIM_EXPIRED_*` (Task 1). +- Produces: complete home-side behavior; nothing new for later tasks. + +- [ ] **Step 1: Add dialog state and the draft branch in HomeScreen** + +In the `HomeScreen` composable (where `startClaimBottomSheetState` lives), add: + +```kotlin +val draftClaim = (uiState as? Success)?.draftClaim +var showDraftClaimDialog by remember { mutableStateOf(false) } +var showDraftExpiredDialog by remember { mutableStateOf(false) } +var draftIdPendingDeleteConfirmation by remember { mutableStateOf(null) } +``` + +Below the `StartClaimBottomSheet(...)` call, add the three dialogs: + +```kotlin +if (showDraftClaimDialog) { + DraftClaimDialog( + onDismissRequest = { showDraftClaimDialog = false }, + onContinueDraft = { + showDraftClaimDialog = false + navigateToClaimChat(true) + }, + onStartNewClaim = { + showDraftClaimDialog = false + startClaimBottomSheetState.show(Unit) + }, + ) +} +if (showDraftExpiredDialog) { + ErrorDialog( + title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), + message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), + onDismiss = { showDraftExpiredDialog = false }, + ) +} +val draftIdToDelete = draftIdPendingDeleteConfirmation +if (draftIdToDelete != null) { + HedvigAlertDialog( + title = stringResource(Res.string.RESUME_CLAIM_DELETE_TITLE), + text = stringResource(Res.string.RESUME_CLAIM_DELETE_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), + dismissButtonLabel = stringResource(Res.string.general_cancel_button), + onDismissRequest = { draftIdPendingDeleteConfirmation = null }, + onConfirmClick = { + draftIdPendingDeleteConfirmation = null + deleteDraftClaim(draftIdToDelete) + }, + ) +} +``` + +`ErrorDialog` and `HedvigAlertDialog` come from `com.hedvig.android.design.system.hedvig` (same components the claim chat uses). + +- [ ] **Step 2: Thread the callbacks** + +- `HomeDestination` (top-level composable): create `deleteDraftClaim = { draftId: String -> viewModel.emit(HomeEvent.DeleteDraftClaim(draftId)) }` and pass it into `HomeScreen` as a new `deleteDraftClaim: (String) -> Unit` parameter (follow the pattern of the existing `reload`/`markMessageAsSeen` lambdas; if events are sent with a different method name than `emit`, match whatever `reload` uses). +- `openClaimFlowSheet` becomes draft-aware: + +```kotlin +openClaimFlowSheet = { + if (draftClaim != null) { + showDraftClaimDialog = true + } else { + startClaimBottomSheetState.show(Unit) + } +}, +``` + +- Add two new params to `HomeScreenSuccess` and thread them into the `claimStatusCards = { ... }` slot: `onContinueDraftClaim: () -> Unit` and `onDeleteDraftClaim: (String) -> Unit`, built in `HomeScreen` as: + +```kotlin +onContinueDraftClaim = { + if (draftClaim != null) { + if (draftClaim.isExpired(Clock.System.now())) { + showDraftExpiredDialog = true + } else { + navigateToClaimChat(true) + } + } +}, +onDeleteDraftClaim = { draftId -> draftIdPendingDeleteConfirmation = draftId }, +``` + +(`Clock` is `kotlin.time.Clock`, matching the `kotlin.time.Instant` in `DraftClaim`.) + +- [ ] **Step 3: Put the draft card first in the cards list** + +In the `claimStatusCards = { ... }` block (Task 5 left the callbacks as no-ops), build the combined list: + +```kotlin +claimStatusCards = { + val claimCards: NonEmptyList? = buildList { + uiState.draftClaim?.let { draftClaim -> + add(ClaimCardUiState.Draft(draftClaim.id, draftClaim.displayName, draftClaim.startedAt)) + } + uiState.claimStatusCardsData?.claimStatusCardsUiState?.forEach { add(ClaimCardUiState.Claim(it)) } + }.toNonEmptyListOrNull() + if (claimCards != null) { + ClaimStatusCards( + onClick = onClaimDetailCardClicked, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, + claimCardsUiState = claimCards, + contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets, + ) + } +}, +``` + +Update the two `PreviewHomeScreen*` fakes for any new required params (previews construct `HomeUiState.Success`, which already has `draftClaim` from Task 7). + +- [ ] **Step 4: Compile and eyeball previews** + +Run: `./gradlew :feature-home:compileDebugKotlin` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 5: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/feature/feature-home +git commit -m "Wire draft claim card, delete and expired dialogs on home" +``` + +--- + +### Task 10: Claim chat: displayName title, resumable-gated leave dialog + +**Files:** +- Modify: `app/feature/feature-claim-chat/build.gradle.kts` (add `implementation(projects.featureFlags)` to `commonMain.dependencies`) +- Modify: `app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt` + +**Interfaces:** +- Consumes: `Feature.ENABLE_CLAIM_INTENT_RESUME` (Task 2), schema `resumable` field (Task 1). +- Produces: `ClaimChatUiState.ClaimChat` gains `title: String?`, `isResumable: Boolean`, `resumeClaimEnabled: Boolean`. Internal `ClaimIntent` gains `displayName: String?`, `resumable: Boolean`. + +- [ ] **Step 1: Fragment fields** + +In `FragmentClaimIntent.graphql`, inside `fragment ClaimIntentFragment on ClaimIntent`, add two fields next to `progress`: + +```graphql + displayName + resumable +``` + +- [ ] **Step 2: Model + mapping** + +`ClaimIntent.kt`: + +```kotlin +internal data class ClaimIntent( + val id: ClaimIntentId, + val next: Next, + val progress: Float?, + val displayName: String?, + val resumable: Boolean, + val previousSteps: List, +) { +``` + +`ClaimIntentExt.kt` `toClaimIntent`: add `displayName = displayName,` and `resumable = resumable,` to the `ClaimIntent(...)` construction. + +- [ ] **Step 3: Feature flag into the presenter** + +`build.gradle.kts` commonMain deps: `implementation(projects.featureFlags)` (alphabetical). + +`ClaimChatViewModel`: add `featureManager: FeatureManager` to the constructor (a normal Metro dependency, placed with the other non-assisted params) and pass it to `ClaimChatPresenter`, which stores `private val featureManager: FeatureManager`. + +- [ ] **Step 4: Track title/isResumable through handleNext** + +In `ClaimChatPresenter.present`, next to the existing `progress` state (~line 250): + +```kotlin +var title by remember { mutableStateOf((lastState as? ClaimChatUiState.ClaimChat)?.title) } +var isResumable by remember { + mutableStateOf((lastState as? ClaimChatUiState.ClaimChat)?.isResumable ?: false) +} +val updateIntentMetadata: (ClaimIntent) -> Unit = { intent -> + progress = intent.progress + // Keep the previous title when a step comes back without one, matching iOS. + title = intent.displayName ?: title + isResumable = intent.resumable +} +val resumeClaimEnabled by remember { + featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME) +}.collectAsState(initial = false) +``` + +(import `androidx.compose.runtime.collectAsState`, `com.hedvig.android.featureflags.FeatureManager`, `com.hedvig.android.featureflags.flags.Feature`.) + +Change the top-level `handleNext` signature (line ~1074) from `setProgress: (Float?) -> Unit` to `updateIntentMetadata: (ClaimIntent) -> Unit`, and its body's `setProgress(intent.progress)` to `updateIntentMetadata(intent)`. Update every call site from `) { progress = it }` to `, updateIntentMetadata)` (9 hits; verify with `rg -n "handleNext\(" app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt`; they are `handleNext(steps, setOutcome, claimIntent) { progress = it }` today). + +In the initializing block, in BOTH the resume and start branches, replace `progress = claimIntent.progress` with `updateIntentMetadata(claimIntent)`. + +Extend the returned state: + +```kotlin +claimIntentId != null -> ClaimChatUiState.ClaimChat( + // ... existing args ... + title = title, + isResumable = isResumable, + resumeClaimEnabled = resumeClaimEnabled, +) +``` + +and the `ClaimChatUiState.ClaimChat` data class gains: + +```kotlin +val title: String?, +val isResumable: Boolean, +val resumeClaimEnabled: Boolean, +``` + +- [ ] **Step 5: Title and leave dialog in the destination** + +In `ClaimChatDestination.kt`: + +Title (line ~359): + +```kotlin +val legacyTitle = stringResource(Res.string.CHAT_CONVERSATION_CLAIM_TITLE) +val title = if (uiState.resumeClaimEnabled) uiState.title ?: legacyTitle else legacyTitle +``` + +Leave-confirmation gating: define next to `showCloseFlowDialog` (line ~279): + +```kotlin +// Flag on: only a resumable draft warrants a leave confirmation (it will be saved). +// Flag off: legacy behavior, always confirm. +val showLeaveConfirmation = if (uiState.resumeClaimEnabled) uiState.isResumable else true +``` + +- `NavigationEventHandler`'s `isBackEnabled = uiState.steps.size > 1` becomes `isBackEnabled = uiState.steps.size > 1 && showLeaveConfirmation`. +- The `TopAppBar` `onActionClick` condition `if (uiState.steps.size > 1)` becomes `if (uiState.steps.size > 1 && showLeaveConfirmation)`. +- The dialog itself (line ~323) becomes: + +```kotlin +if (showCloseFlowDialog) { + if (uiState.resumeClaimEnabled) { + HedvigAlertDialog( + title = stringResource(Res.string.RESUME_CLAIM_LEAVE_TITLE), + text = stringResource(Res.string.RESUME_CLAIM_LEAVE_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_LEAVE_CONFIRM), + onDismissRequest = { showCloseFlowDialog = false }, + onConfirmClick = navigateUp, + ) + } else { + HedvigAlertDialog( + title = stringResource(Res.string.GENERAL_ARE_YOU_SURE), + text = stringResource(Res.string.claims_alert_body), + onDismissRequest = { showCloseFlowDialog = false }, + onConfirmClick = navigateUp, + ) + } +} +``` + +(The dismiss label defaults to `GENERAL_NO`, which matches "No". This removes the hardcoded "Your answers will be saved in a draft claim" TODO line and its commented-out predecessor.) + +- [ ] **Step 6: Compile and test** + +Run: `./gradlew :feature-claim-chat:compileDebugKotlinAndroid :feature-claim-chat:test` +Expected: BUILD SUCCESSFUL, tests (if the module has any) PASS. + +- [ ] **Step 7: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/feature/feature-claim-chat +git commit -m "Use displayName title and resumable-gated leave dialog in claim chat" +``` + +--- + +### Task 11: Inbox draft handling + +**Files:** +- Modify: `app/feature/feature-chat/build.gradle.kts` (add `implementation(projects.dataClaimIntent)`) +- Modify: `app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt` +- Modify: `app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt` + +**Interfaces:** +- Consumes: `GetResumableClaimIntentUseCase` (Task 3), `Feature.ENABLE_CLAIM_INTENT_RESUME` (Task 2), `DraftClaimDialog` (Task 4), `navigateToClaimChat: (Boolean) -> Unit` (Task 6). +- Produces: `InboxUiState.Success` gains `hasDraftClaim: Boolean`. + +- [ ] **Step 1: Dependency** + +`app/feature/feature-chat/build.gradle.kts`: add `implementation(projects.dataClaimIntent)` (alphabetical). + +- [ ] **Step 2: Fetch the draft in InboxPresenter** + +`InboxViewModel.kt`: + +```kotlin +@Inject +@HedvigViewModel(ActivityRetainedScope::class) +internal class InboxViewModel( + getAllConversationsUseCase: GetAllConversationsUseCase, + featureManager: FeatureManager, + getResumableClaimIntentUseCase: GetResumableClaimIntentUseCase, +) : MoleculeViewModel( + initialState = InboxUiState.Loading, + presenter = InboxPresenter(getAllConversationsUseCase, featureManager, getResumableClaimIntentUseCase), + ) + +internal class InboxPresenter( + private val getAllConversationsUseCase: GetAllConversationsUseCase, + private val featureManager: FeatureManager, + private val getResumableClaimIntentUseCase: GetResumableClaimIntentUseCase, +) : MoleculePresenter { +``` + +Inside `LaunchedEffect(loadIteration)`, before the existing `combine`: + +```kotlin +val hasDraftClaim = if (featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME).first()) { + getResumableClaimIntentUseCase.invoke().fold( + // A failed draft lookup must not block starting a claim; treat it as no draft. + ifLeft = { false }, + ifRight = { it != null }, + ) +} else { + false +} +``` + +and extend the success state construction: `currentState = InboxUiState.Success(conversations, newChatButtonAvailable, hasDraftClaim)`. `InboxUiState.Success` gains `val hasDraftClaim: Boolean`. Imports: `com.hedvig.android.data.claimintent.GetResumableClaimIntentUseCase`, `kotlinx.coroutines.flow.first`. + +- [ ] **Step 3: Show the dialog before the pledge sheet** + +In `InboxDestination.kt`'s `InboxScreen`, add next to the sheet states: + +```kotlin +var showDraftClaimDialog by remember { mutableStateOf(false) } +if (showDraftClaimDialog) { + DraftClaimDialog( + onDismissRequest = { showDraftClaimDialog = false }, + onContinueDraft = { + showDraftClaimDialog = false + navigateToClaimChat(true) + }, + onStartNewClaim = { + showDraftClaimDialog = false + startClaimBottomSheetState.show(Unit) + }, + ) +} +``` + +and change the `onStartNewClaim` callback inside the new-chat-select sheet: + +```kotlin +onStartNewClaim = { + newChatSelectBottomSheetState.dismiss() + if ((uiState as? InboxUiState.Success)?.hasDraftClaim == true) { + showDraftClaimDialog = true + } else { + startClaimBottomSheetState.show(Unit) + } +}, +``` + +Import `com.hedvig.android.design.system.hedvig.DraftClaimDialog` plus `androidx.compose.runtime.getValue/mutableStateOf/remember/setValue` as needed. + +- [ ] **Step 4: Compile and test** + +Run: `./gradlew :feature-chat:compileDebugKotlin :feature-chat:test` +Expected: BUILD SUCCESSFUL; existing tests PASS. If an `InboxPresenterTest` exists, add the fake use case there the same way Task 8 did for home; if none exists, do not create new test infrastructure (the branching logic mirrors home's, which is tested). + +- [ ] **Step 5: Commit** + +```bash +./gradlew ktlintFormat +git add -A app/feature/feature-chat +git commit -m "Show draft claim dialog from the inbox start-claim entry" +``` + +--- + +### Task 12: Cleanups and full verification + +**Files:** +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt` +- Modify: `app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt` + +**Interfaces:** none new; this task closes out branch debt and verifies the whole feature. + +- [ ] **Step 1: Fix the copy-pasted log tag** + +In `ResumeClaimUseCase.kt`, `logcat { "StartClaimIntentUseCase error: $it" }` becomes `logcat { "ResumeClaimUseCase error: $it" }`. Also switch it to the Apollo overload for consistency: `logcat(operationError = it) { "ResumeClaimUseCase failed with $it" }` (same import as in Task 3's use cases). + +- [ ] **Step 2: Resolve the isPrepared TODO** + +In `ClaimIntentExt.kt`, the resumed-audio mapping has `isPrepared = true, // TODO: check`. Replace the comment: + +```kotlin +AudioRecordingStepState.AudioRecording.Playback( + audioPath = AudioPath.RemoteUrl(audioUrl), + isPlaying = false, + // A resumed remote recording has no local MediaPlayer to prepare; the remote audio player + // handles its own buffering, so the playback UI can show immediately. + isPrepared = true, + hasError = false, +) +``` + +Verify during manual QA (Step 5) that a resumed audio step actually plays via `PlayableAudioSource.RemoteUrl`. The `AudioRecordingBottomSheet` returning a `null` player for remote URLs is fine: that sheet is only reachable while recording, before a step was ever submitted. + +- [ ] **Step 3: Format and full test run** + +```bash +./gradlew ktlintFormat +./gradlew ktlintCheck +./gradlew test +``` + +Expected: all PASS (includes `ExhaustiveBackStackSerializationTest` and `BackstackTest` in `:app`, `HomePresenterTest`, and the feature-claim-chat tests). + +- [ ] **Step 4: Full app assembly** + +Run: `./gradlew :app:assembleDevelop` +Expected: BUILD SUCCESSFUL. + +- [ ] **Step 5: Manual QA checklist (staging build, flag on in Unleash)** + +Home refresh mechanics note: `MoleculeViewModel` uses `SharingStarted.WhileSubscribed(5.seconds)`, so leaving home for the claim chat for more than 5 seconds stops the home presenter; returning restarts it and re-runs the Home query with `CacheAndNetwork`. This is the expected refresh-on-return mechanism; verify it in item 2 and only add an explicit reload trigger if it does not hold. + +1. Start a claim, answer a step, leave via the top-bar X: "Leave claim?" dialog with "No" / "Yes, leave" appears only once a resumable step was reached. +2. Back on home: the Draft card appears first in the cards carousel (amber Draft pill, title, "Started {date}", inactive segments). If it does not appear without a manual refresh, add `viewModel.emit(HomeEvent.RefreshData)` in a `LifecycleResumeEffect` and re-verify. +3. Card "Continue": resumes the chat with previous steps replayed, no honesty pledge, title from displayName. +4. Card "Delete": "Delete draft?" confirm, then the card disappears. +5. Home "Make a claim" with a draft: "You have a draft claim" dialog; "Continue draft" resumes; "Start new claim" opens the pledge sheet; "Cancel" does nothing. +6. Inbox → new conversation → "Start claim" with a draft: same dialog behavior. +7. A draft whose current step is a deflect outcome resumes into the deflect screen. +8. Resumed audio step: remote recording plays; resumed free-text step shows the saved text. +9. Flag off in Unleash: no draft card, no draft dialogs, legacy chat title, legacy always-on leave dialog. +10. Demo mode: home loads with no draft card. + +- [ ] **Step 6: Commit** + +```bash +git add -A app/ +git commit -m "Clean up resume-claim leftovers" +``` From b6cefb4d9716b3803ae4c64fb39ca9ce7a5f6980 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:10:21 +0200 Subject: [PATCH 11/28] Download schema --- .../com/hedvig/android/apollo/octopus/schema.graphqls | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls index 9b640dcbe3..98edc25fe0 100644 --- a/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls +++ b/app/apollo/apollo-octopus-public/src/commonMain/graphql/com/hedvig/android/apollo/octopus/schema.graphqls @@ -971,6 +971,11 @@ type ClaimIntent { """ displayName: String """ + Whether this draft is resumable by the member. A draft that has been deleted (see `claimIntentDeleteDraft`) + is no longer resumable. + """ + resumable: Boolean! + """ The created claim of this intent. Once this value is returned, the intent is effectively over, and no steps can be produced or interacted with. @@ -3713,6 +3718,10 @@ type Mutation { """ claimIntentRegretStep(stepId: ID!): ClaimIntentMutationOutput! """ + Delete a draft claim intent from the member's perspective (a soft delete) so it is no longer returned by `resumableClaimIntent`. + """ + claimIntentDeleteDraft(id: ID!): Boolean! + """ Submit a step containing a `ClaimIntentStepContentForm`. """ claimIntentSubmitForm(input: ClaimIntentSubmitFormInput!): ClaimIntentMutationOutput! From fd9bed8f2b02f08c533110484b74c326942eeb71 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:11:32 +0200 Subject: [PATCH 12/28] Download strings with RESUME_CLAIM keys --- .../androidMain/res/values-sv-rSE/strings.xml | 22 +++++++++++++++++-- .../src/androidMain/res/values/strings.xml | 18 +++++++++++++++ .../values-sv-rSE/strings.xml | 22 +++++++++++++++++-- .../composeResources/values/strings.xml | 18 +++++++++++++++ 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml index 75a8dca92c..14e7373a3f 100644 --- a/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values-sv-rSE/strings.xml @@ -503,8 +503,8 @@ Inga konversationer än. Starta en genom att skriva ett meddelande Din inkorg är tom Nytt - Anmäl något som har hänt med dig eller dina saker - Frågor om din försäkring, betalningar och annat + Anmäl något som hänt dig eller dina saker + Frågor om din försäkring, betalningar eller annat Erbjudanden Du har ingen aktiv försäkring Tillägg @@ -714,6 +714,23 @@ Den här ändringar börjar gälla %s. Tillägg avslutade Ja, radera + Fortsätt + Försäkringsärende + Ditt sparade utkast raderas permanent. + Radera + Radera utkast? + Draft + Om du startar en ny anmälan raderas ditt sparade utkast + Fortsätt anmälan + Starta ny anmälan + Du har en påbörjad anmälan + Vänligen starta en ny anmälan + Ditt utkast har gått ut + Fortsätt där du slutade + Din anmälan sparas automatiskt i 7 dagar. + Ja, lämna + Lämna anmälan? + Borjade %1$s Spara och fortsätt Inga träffar på din sökning Sök @@ -986,6 +1003,7 @@ Välj den försäkring du vill uppdatera Visa ditt skydd Välj din skyddsnivå och självrisk + Välj din skyddsnivå Godkänn ändringar Skyddet Dokument diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 0d194724d5..272df7a2a4 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -714,6 +714,23 @@ This change will take effect from %s. Removed successfully Yes, remove + Continue + Insurance case + Your saved draft will be permanently deleted. + Delete + Delete draft? + Draft + Starting a new claim will delete your saved draft + Continue draft + Start new claim + You have a draft claim + Please make a new claim + Please make a new claim + Continue where you stopped + Your claim is automatically saved for 7 days. + Yes, leave + Leave claim? + Started %1$s Save and continue No results for your search Search @@ -986,6 +1003,7 @@ Select the insurance you want to edit Show coverage Set your coverage level and deductible + Set your coverage level Confirm changes Coverage Documents diff --git a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml index 2d75e502d1..12f10933e9 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values-sv-rSE/strings.xml @@ -502,8 +502,8 @@ Inga konversationer än. Starta en genom att skriva ett meddelande Din inkorg är tom Nytt - Anmäl något som har hänt med dig eller dina saker - Frågor om din försäkring, betalningar och annat + Anmäl något som hänt dig eller dina saker + Frågor om din försäkring, betalningar eller annat Erbjudanden Du har ingen aktiv försäkring Tillägg @@ -713,6 +713,23 @@ Den här ändringar börjar gälla %1$s. Tillägg avslutade Ja, radera + Fortsätt + Försäkringsärende + Ditt sparade utkast raderas permanent. + Radera + Radera utkast? + Draft + Om du startar en ny anmälan raderas ditt sparade utkast + Fortsätt anmälan + Starta ny anmälan + Du har en påbörjad anmälan + Vänligen starta en ny anmälan + Ditt utkast har gått ut + Fortsätt där du slutade + Din anmälan sparas automatiskt i 7 dagar. + Ja, lämna + Lämna anmälan? + Borjade %1$s Spara och fortsätt Inga träffar på din sökning Sök @@ -985,6 +1002,7 @@ Välj den försäkring du vill uppdatera Visa ditt skydd Välj din skyddsnivå och självrisk + Välj din skyddsnivå Godkänn ändringar Skyddet Dokument diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 0061307317..08b7376726 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -713,6 +713,23 @@ This change will take effect from %1$s. Removed successfully Yes, remove + Continue + Insurance case + Your saved draft will be permanently deleted. + Delete + Delete draft? + Draft + Starting a new claim will delete your saved draft + Continue draft + Start new claim + You have a draft claim + Please make a new claim + Please make a new claim + Continue where you stopped + Your claim is automatically saved for 7 days. + Yes, leave + Leave claim? + Started %1$s Save and continue No results for your search Search @@ -985,6 +1002,7 @@ Select the insurance you want to edit Show coverage Set your coverage level and deductible + Set your coverage level Confirm changes Coverage Documents From a53fec7fa9849d7e5fcf12f67e406e10788b6469 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:17:21 +0200 Subject: [PATCH 13/28] Add enable_claim_intent_resume feature flag --- .../android/featureflags/flags/FeatureUnleashKey.kt | 3 ++- .../com/hedvig/android/featureflags/flags/Feature.kt | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt index 365edbcc1b..cc610ec0dd 100644 --- a/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt +++ b/app/featureflags/feature-flags/src/androidMain/kotlin/com/hedvig/android/featureflags/flags/FeatureUnleashKey.kt @@ -2,7 +2,8 @@ package com.hedvig.android.featureflags.flags internal val Feature.unleashKey: String get() = when (this) { + Feature.DISABLE_PUPPY_GUIDE -> "disable_puppy_guide" + Feature.ENABLE_CLAIM_INTENT_RESUME -> "enable_claim_intent_resume" Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX -> "enable_new_conversation_from_inbox" Feature.UPDATE_NECESSARY -> "update_necessary" - Feature.DISABLE_PUPPY_GUIDE -> "disable_puppy_guide" } diff --git a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt index 38a8d870ed..bcd389b963 100644 --- a/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt +++ b/app/featureflags/feature-flags/src/commonMain/kotlin/com/hedvig/android/featureflags/flags/Feature.kt @@ -4,6 +4,13 @@ enum class Feature( // Used to easier get a context of what it's for. @Suppress("unused") val explanation: String, ) { + DISABLE_PUPPY_GUIDE( + "Kill switch for the puppy guide in the help center. When the toggle is on, the puppy guide is hidden.", + ), + ENABLE_CLAIM_INTENT_RESUME( + "Enables resuming a draft claim: the draft card on the home screen, the draft-claim dialogs, " + + "and the resumable-aware leave dialog in the claim chat.", + ), ENABLE_NEW_CONVERSATION_FROM_INBOX( "Enables inbox icon always available on the Home screen " + "and New conversation button inside the inbox", @@ -11,7 +18,4 @@ enum class Feature( UPDATE_NECESSARY( "Defines the lowest supported app version. Should prompt a user to update if it uses an outdated version.", ), - DISABLE_PUPPY_GUIDE( - "Kill switch for the puppy guide in the help center. When the toggle is on, the puppy guide is hidden.", - ), } From b09a034ac9096f7aedbf8668b8a6e77fdc3084d0 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:26:55 +0200 Subject: [PATCH 14/28] Add data-claim-intent module with resumable draft get and delete use cases --- app/data/data-claim-intent/build.gradle.kts | 21 ++++++++ .../MutationClaimIntentDeleteDraft.graphql | 3 ++ .../graphql/QueryResumableClaimIntent.graphql | 9 ++++ .../DeleteClaimIntentDraftUseCase.kt | 38 ++++++++++++++ .../GetResumableClaimIntentUseCase.kt | 50 +++++++++++++++++++ .../data/claimintent/ResumableClaimIntent.kt | 9 ++++ 6 files changed, 130 insertions(+) create mode 100644 app/data/data-claim-intent/build.gradle.kts create mode 100644 app/data/data-claim-intent/src/commonMain/graphql/MutationClaimIntentDeleteDraft.graphql create mode 100644 app/data/data-claim-intent/src/commonMain/graphql/QueryResumableClaimIntent.graphql create mode 100644 app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/DeleteClaimIntentDraftUseCase.kt create mode 100644 app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/GetResumableClaimIntentUseCase.kt create mode 100644 app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/ResumableClaimIntent.kt diff --git a/app/data/data-claim-intent/build.gradle.kts b/app/data/data-claim-intent/build.gradle.kts new file mode 100644 index 0000000000..fba12973f6 --- /dev/null +++ b/app/data/data-claim-intent/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("hedvig.multiplatform.library") + id("hedvig.gradle.plugin") +} + +hedvig { + apollo("octopus") +} + +kotlin { + sourceSets { + commonMain.dependencies { + implementation(libs.apollo.normalizedCache) + implementation(libs.arrow.core) + implementation(projects.apolloCore) + implementation(projects.apolloOctopusPublic) + implementation(projects.coreCommonPublic) + implementation(projects.loggingPublic) + } + } +} diff --git a/app/data/data-claim-intent/src/commonMain/graphql/MutationClaimIntentDeleteDraft.graphql b/app/data/data-claim-intent/src/commonMain/graphql/MutationClaimIntentDeleteDraft.graphql new file mode 100644 index 0000000000..e0e7c78cff --- /dev/null +++ b/app/data/data-claim-intent/src/commonMain/graphql/MutationClaimIntentDeleteDraft.graphql @@ -0,0 +1,3 @@ +mutation ClaimIntentDeleteDraft($id: ID!) { + claimIntentDeleteDraft(id: $id) +} diff --git a/app/data/data-claim-intent/src/commonMain/graphql/QueryResumableClaimIntent.graphql b/app/data/data-claim-intent/src/commonMain/graphql/QueryResumableClaimIntent.graphql new file mode 100644 index 0000000000..fea47c6e9a --- /dev/null +++ b/app/data/data-claim-intent/src/commonMain/graphql/QueryResumableClaimIntent.graphql @@ -0,0 +1,9 @@ +query ResumableClaimIntent { + currentMember { + resumableClaimIntent { + id + displayName + createdAt + } + } +} diff --git a/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/DeleteClaimIntentDraftUseCase.kt b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/DeleteClaimIntentDraftUseCase.kt new file mode 100644 index 0000000000..4290082106 --- /dev/null +++ b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/DeleteClaimIntentDraftUseCase.kt @@ -0,0 +1,38 @@ +package com.hedvig.android.data.claimintent + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.apollographql.apollo.ApolloClient +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.logger.logcat +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import octopus.ClaimIntentDeleteDraftMutation + +interface DeleteClaimIntentDraftUseCase { + suspend fun invoke(id: String): Either +} + +@Inject +@ContributesBinding(AppScope::class) +internal class DeleteClaimIntentDraftUseCaseImpl( + private val apolloClient: ApolloClient, +) : DeleteClaimIntentDraftUseCase { + override suspend fun invoke(id: String): Either { + return either { + val deleted = apolloClient + .mutation(ClaimIntentDeleteDraftMutation(id)) + .safeExecute() + .mapLeft { error -> + logcat(operationError = error) { "DeleteClaimIntentDraftUseCase failed with $error" } + ErrorMessage() + } + .bind() + .claimIntentDeleteDraft + ensure(deleted) { ErrorMessage() } + } + } +} diff --git a/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/GetResumableClaimIntentUseCase.kt b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/GetResumableClaimIntentUseCase.kt new file mode 100644 index 0000000000..3315bb773f --- /dev/null +++ b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/GetResumableClaimIntentUseCase.kt @@ -0,0 +1,50 @@ +package com.hedvig.android.data.claimintent + +import arrow.core.Either +import arrow.core.raise.either +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.cache.normalized.FetchPolicy +import com.apollographql.apollo.cache.normalized.fetchPolicy +import com.hedvig.android.apollo.safeExecute +import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.core.common.di.AppScope +import com.hedvig.android.logger.logcat +import dev.zacsweers.metro.ContributesBinding +import dev.zacsweers.metro.Inject +import octopus.ResumableClaimIntentQuery + +interface GetResumableClaimIntentUseCase { + /** + * A null right side means the member has no resumable draft claim. + */ + suspend fun invoke(): Either +} + +@Inject +@ContributesBinding(AppScope::class) +internal class GetResumableClaimIntentUseCaseImpl( + private val apolloClient: ApolloClient, +) : GetResumableClaimIntentUseCase { + override suspend fun invoke(): Either { + return either { + apolloClient + .query(ResumableClaimIntentQuery()) + .fetchPolicy(FetchPolicy.NetworkOnly) + .safeExecute() + .mapLeft { error -> + logcat(operationError = error) { "GetResumableClaimIntentUseCase failed with $error" } + ErrorMessage() + } + .bind() + .currentMember + .resumableClaimIntent + ?.let { resumableClaimIntent -> + ResumableClaimIntent( + id = resumableClaimIntent.id, + displayName = resumableClaimIntent.displayName, + startedAt = resumableClaimIntent.createdAt, + ) + } + } + } +} diff --git a/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/ResumableClaimIntent.kt b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/ResumableClaimIntent.kt new file mode 100644 index 0000000000..143b45763b --- /dev/null +++ b/app/data/data-claim-intent/src/commonMain/kotlin/com/hedvig/android/data/claimintent/ResumableClaimIntent.kt @@ -0,0 +1,9 @@ +package com.hedvig.android.data.claimintent + +import kotlin.time.Instant + +data class ResumableClaimIntent( + val id: String, + val displayName: String?, + val startedAt: Instant, +) From 6827abf4dfc1b1d708c468378bb4d0532d3f5dcb Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:35:08 +0200 Subject: [PATCH 15/28] Revert draft entry from pledge sheet and add DraftClaimDialog --- .../design/system/hedvig/DraftClaimDialog.kt | 79 +++++++++++++++++++ .../system/hedvig/StartClaimBottomSheet.kt | 61 ++------------ .../feature/chat/inbox/InboxDestination.kt | 6 +- .../feature/home/home/ui/HomeDestination.kt | 13 +-- 4 files changed, 93 insertions(+), 66 deletions(-) create mode 100644 app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/DraftClaimDialog.kt diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/DraftClaimDialog.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/DraftClaimDialog.kt new file mode 100644 index 0000000000..0642f4f140 --- /dev/null +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/DraftClaimDialog.kt @@ -0,0 +1,79 @@ +package com.hedvig.android.design.system.hedvig + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_BODY +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_CONTINUE +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_START_NEW +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_TITLE +import hedvig.resources.Res +import hedvig.resources.general_cancel_button +import org.jetbrains.compose.resources.stringResource + +@Composable +fun DraftClaimDialog( + onDismissRequest: () -> Unit, + onContinueDraft: () -> Unit, + onStartNewClaim: () -> Unit, + modifier: Modifier = Modifier, +) { + HedvigDialog( + onDismissRequest = onDismissRequest, + modifier = modifier, + ) { + Column { + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_TITLE), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_BODY), + textAlign = TextAlign.Center, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(24.dp)) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_CONTINUE), + onClick = onContinueDraft, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_START_NEW), + onClick = onStartNewClaim, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Red, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigButton( + text = stringResource(Res.string.general_cancel_button), + onClick = onDismissRequest, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Ghost, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@HedvigPreview +@Composable +private fun PreviewDraftClaimDialog() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + DraftClaimDialog({}, {}, {}) + } + } +} diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt index 6b168aeef1..bdea2a83c2 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/StartClaimBottomSheet.kt @@ -23,10 +23,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.dropUnlessResumed -import com.hedvig.android.compose.ui.preview.BooleanCollectionPreviewParameterProvider import com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState import com.hedvig.android.design.system.hedvig.icon.Checkmark import com.hedvig.android.design.system.hedvig.icon.HedvigIcons @@ -42,13 +40,8 @@ import hedvig.resources.general_cancel_button import hedvig.resources.general_continue_button import org.jetbrains.compose.resources.stringResource -data class StartClaimSheetData(val resumableClaimId: String?) @Composable -fun StartClaimBottomSheet( - state: HedvigBottomSheetState, - navigateToClaimChat: () -> Unit, - navigateToOldClaim: () -> Unit, -) { +fun StartClaimBottomSheet(state: HedvigBottomSheetState, navigateToClaimChat: () -> Unit) { HedvigBottomSheet( hedvigBottomSheetState = state, content = { @@ -61,27 +54,18 @@ fun StartClaimBottomSheet( navigateToClaimChat() } }, - navigateToOldClaim = { - state.dismiss { - navigateToOldClaim() - } - }, - resumableClaimId = state.data?.resumableClaimId ) }, ) } - @Composable -fun StartClaimPledgeScreen( - navigateUp: () -> Unit, - navigateToClaimChat: () -> Unit, - modifier: Modifier = Modifier, -) { +fun StartClaimPledgeScreen(navigateUp: () -> Unit, navigateToClaimChat: () -> Unit, modifier: Modifier = Modifier) { var isChecked by remember { mutableStateOf(false) } - Column(modifier - .verticalScroll(rememberScrollState())) { + Column( + modifier + .verticalScroll(rememberScrollState()), + ) { PledgeNotes() Spacer(Modifier.weight(1f)) Spacer(Modifier.height(8.dp)) @@ -92,8 +76,6 @@ fun StartClaimPledgeScreen( }, navigateToClaimChat = navigateToClaimChat, dismiss = navigateUp, - resumableClaimId = null, - navigateToOldClaim = {} ) Spacer(Modifier.height(8.dp)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) @@ -101,12 +83,7 @@ fun StartClaimPledgeScreen( } @Composable -private fun StartClaimBottomSheetContent( - dismiss: () -> Unit, - navigateToClaimChat: () -> Unit, - navigateToOldClaim: () -> Unit, - resumableClaimId: String? -) { +private fun StartClaimBottomSheetContent(dismiss: () -> Unit, navigateToClaimChat: () -> Unit) { var isChecked by remember { mutableStateOf(false) } Column { Spacer(Modifier.height(16.dp)) @@ -127,8 +104,6 @@ private fun StartClaimBottomSheetContent( }, navigateToClaimChat = navigateToClaimChat, dismiss = dismiss, - navigateToOldClaim = navigateToOldClaim, - resumableClaimId = resumableClaimId ) Spacer(Modifier.height(8.dp)) Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.safeDrawing)) @@ -141,8 +116,6 @@ private fun StartClaimBottomContent( onCheckedChange: () -> Unit, navigateToClaimChat: () -> Unit, dismiss: () -> Unit, - navigateToOldClaim: () -> Unit, - resumableClaimId: String? ) { Column { ImportantInfoCheckBox( @@ -159,18 +132,6 @@ private fun StartClaimBottomContent( modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(16.dp)) - if (resumableClaimId!=null) { - HedvigButton( - buttonStyle = ButtonDefaults.ButtonStyle.PrimaryAlt, - text = "Continue with the draft claim", - enabled = true, - onClick = dropUnlessResumed { - navigateToOldClaim() - }, - modifier = Modifier.fillMaxWidth(), - ) - Spacer(Modifier.height(16.dp)) - } HedvigButton( text = stringResource(Res.string.general_cancel_button), enabled = true, @@ -246,18 +207,12 @@ private fun ImportantInfoCheckBox(isChecked: Boolean, onCheckedChange: () -> Uni @HedvigPreview @Composable -private fun PreviewStartClaimBottomSheetContent( - @PreviewParameter( - BooleanCollectionPreviewParameterProvider::class, - ) hasResumableClaim: Boolean, -) { +private fun PreviewStartClaimBottomSheetContent() { HedvigTheme { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { StartClaimBottomSheetContent( {}, navigateToClaimChat = {}, - navigateToOldClaim = {}, - resumableClaimId = if (hasResumableClaim) "" else null ) } } diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt index 59d8410e99..390005cba1 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt @@ -61,7 +61,6 @@ import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightS import com.hedvig.android.design.system.hedvig.HorizontalItemsWithMaximumSpaceTaken import com.hedvig.android.design.system.hedvig.Icon import com.hedvig.android.design.system.hedvig.StartClaimBottomSheet -import com.hedvig.android.design.system.hedvig.StartClaimSheetData import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TopAppBar import com.hedvig.android.design.system.hedvig.TopAppBarActionType @@ -132,7 +131,7 @@ private fun InboxScreen( navigateToClaimChat: () -> Unit, ) { val newChatSelectBottomSheetState = rememberHedvigBottomSheetState() - val startClaimBottomSheetState = rememberHedvigBottomSheetState() + val startClaimBottomSheetState = rememberHedvigBottomSheetState() HedvigBottomSheet( newChatSelectBottomSheetState, content = { @@ -143,7 +142,7 @@ private fun InboxScreen( }, onStartNewClaim = { newChatSelectBottomSheetState.dismiss() - startClaimBottomSheetState.show(StartClaimSheetData(null)) + startClaimBottomSheetState.show(Unit) }, dismiss = { newChatSelectBottomSheetState.dismiss() @@ -157,7 +156,6 @@ private fun InboxScreen( startClaimBottomSheetState.dismiss() navigateToClaimChat() }, - navigateToOldClaim = {} ) Surface( color = HedvigTheme.colorScheme.backgroundPrimary, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 9ef4a72fa3..f3f98b4c7e 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -86,7 +86,6 @@ import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.NotificationDefaults import com.hedvig.android.design.system.hedvig.NotificationDefaults.NotificationPriority import com.hedvig.android.design.system.hedvig.StartClaimBottomSheet -import com.hedvig.android.design.system.hedvig.StartClaimSheetData import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.design.system.hedvig.TooltipDefaults import com.hedvig.android.design.system.hedvig.TooltipDefaults.BeakDirection.TopEnd @@ -239,13 +238,9 @@ private fun HomeScreen( imageLoader = imageLoader, ) - val resumableClaimId = (uiState as? Success)?.resumableClaimId - val startClaimBottomSheetState = rememberHedvigBottomSheetState() + val startClaimBottomSheetState = rememberHedvigBottomSheetState() StartClaimBottomSheet( state = startClaimBottomSheetState, - navigateToOldClaim = { - navigateToClaimChat(resumableClaimId) - }, navigateToClaimChat = { navigateToClaimChat(null) }, @@ -286,7 +281,7 @@ private fun HomeScreen( navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, openClaimFlowSheet = { - startClaimBottomSheetState.show(StartClaimSheetData(resumableClaimId)) + startClaimBottomSheetState.show(Unit) }, openAppSettings = openAppSettings, openUrl = openUrl, @@ -806,7 +801,7 @@ private fun PreviewHomeScreen( flowType = FlowType.APP_TRAVEL_PLUS_SELL_OR_UPGRADE, ), isProduction = true, - resumableClaimId = null + resumableClaimId = null, ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, @@ -892,7 +887,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( chatAction = ChatAction, addonBannerInfo = null, isProduction = true, - resumableClaimId = null + resumableClaimId = null, ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, From b4d1afe57aa5fd50c4c6b5c3e1ce34fffc0ddb08 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 18:43:35 +0200 Subject: [PATCH 16/28] Add draft claim card to the claim status cards pager --- .../home/home/data/GetHomeDataUseCase.kt | 4 +- .../home/home/data/GetHomeDataUseCaseDemo.kt | 2 +- .../feature/home/home/ui/HomeDestination.kt | 11 +- .../feature/home/home/ui/HomePresenter.kt | 10 +- .../feature/home/home/ui/HomePresenterTest.kt | 46 +++---- .../ui/claimstatus/ClaimStatusCards.kt | 88 ++++++++++--- .../android/ui/claimstatus/DraftClaimCard.kt | 120 ++++++++++++++++++ .../claimstatus/internal/ClaimProgressRow.kt | 6 + .../ui/claimstatus/model/ClaimCardUiState.kt | 13 ++ .../claimstatus/model/ClaimProgressSegment.kt | 1 + 10 files changed, 240 insertions(+), 61 deletions(-) create mode 100644 app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/DraftClaimCard.kt create mode 100644 app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimCardUiState.kt diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index 86a890c4c1..d5b9335946 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -172,7 +172,7 @@ internal class GetHomeDataUseCaseImpl( crossSells = crossSells, travelBannerInfo = travelBannerInfo?.firstOrNull(), showChatIcon = showChatIcon, - resumableClaimId = homeQueryData.currentMember.resumableClaimIntent?.id + resumableClaimId = homeQueryData.currentMember.resumableClaimIntent?.id, ) }.onLeft { error: ApolloOperationError -> logcat(operationError = error) { "GetHomeDataUseCase failed with $error" } @@ -281,7 +281,7 @@ data class HomeData( val firstVetSections: List, val crossSells: CrossSellSheetData, val travelBannerInfo: AddonBannerInfo?, - val resumableClaimId: String? + val resumableClaimId: String?, ) { @Immutable data class ClaimStatusCardsData( diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index fc50dfc2ea..f40fc3da7c 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -56,7 +56,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ), travelBannerInfo = null, showChatIcon = false, - resumableClaimId = null + resumableClaimId = null, ).right(), ) } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index f3f98b4c7e..14f901fc0f 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import arrow.core.nonEmptyListOf +import arrow.core.toNonEmptyListOrNull import coil3.ImageLoader import com.google.accompanist.permissions.isGranted import com.hedvig.android.compose.pager.indicator.HorizontalPagerIndicator @@ -119,6 +120,7 @@ import com.hedvig.android.pullrefresh.PullRefreshState import com.hedvig.android.pullrefresh.pullRefresh import com.hedvig.android.pullrefresh.rememberPullRefreshState import com.hedvig.android.ui.claimstatus.ClaimStatusCards +import com.hedvig.android.ui.claimstatus.model.ClaimCardUiState import com.hedvig.android.ui.claimstatus.model.ClaimPillType.Claim import com.hedvig.android.ui.claimstatus.model.ClaimPillType.Closed.NotCompensated import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment @@ -478,10 +480,15 @@ private fun HomeScreenSuccess( ) }, claimStatusCards = { - if (uiState.claimStatusCardsData != null) { + val claimCards = uiState.claimStatusCardsData?.claimStatusCardsUiState + ?.map { ClaimCardUiState.Claim(it) } + ?.toNonEmptyListOrNull() + if (claimCards != null) { ClaimStatusCards( onClick = onClaimDetailCardClicked, - claimStatusCardsUiState = uiState.claimStatusCardsData.claimStatusCardsUiState, + onContinueDraftClaim = {}, + onDeleteDraftClaim = {}, + claimCardsUiState = claimCards, contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets, ) } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 3569e4b513..5f97b02d94 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -138,7 +138,7 @@ internal class HomePresenter( crossSellsAction = successData.crossSellsAction, addonBannerInfo = successData.addonBannerInfo, isProduction = isProduction, - resumableClaimId = successData.resumableClaimId + resumableClaimId = successData.resumableClaimId, ) } } @@ -178,7 +178,7 @@ internal sealed interface HomeUiState { val isProduction: Boolean, override val isHelpCenterEnabled: Boolean, override val hasUnseenChatMessages: Boolean, - val resumableClaimId: String? + val resumableClaimId: String?, ) : HomeUiState data class Error(val message: String?) : HomeUiState @@ -197,7 +197,7 @@ private data class SuccessData( val crossSellsAction: HomeTopBarAction.CrossSellsAction?, val hasUnseenChatMessages: Boolean, val addonBannerInfo: AddonBannerInfo?, - val resumableClaimId: String? + val resumableClaimId: String?, ) { companion object { fun fromLastState(lastState: HomeUiState): SuccessData? { @@ -213,7 +213,7 @@ private data class SuccessData( hasUnseenChatMessages = lastState.hasUnseenChatMessages, addonBannerInfo = lastState.addonBannerInfo, chatAction = lastState.chatAction, - resumableClaimId = lastState.resumableClaimId + resumableClaimId = lastState.resumableClaimId, ) } @@ -261,7 +261,7 @@ private data class SuccessData( hasUnseenChatMessages = homeData.hasUnseenChatMessages, addonBannerInfo = homeData.travelBannerInfo, chatAction = if (homeData.showChatIcon) HomeTopBarAction.ChatAction else null, - resumableClaimId = homeData.resumableClaimId + resumableClaimId = homeData.resumableClaimId, ) } } diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index c122a4ee87..620c01199a 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -143,7 +143,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(testCrossSell, listOf()), firstVetSections = listOf(), travelBannerInfo = null, - resumableClaimId = null + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -175,8 +175,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -210,8 +209,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -230,8 +228,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -289,8 +286,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf()), travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()) @@ -326,8 +322,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -344,8 +339,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -384,8 +378,7 @@ internal class HomePresenterTest { ), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -402,8 +395,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -441,8 +433,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -463,7 +454,7 @@ internal class HomePresenterTest { ), addonBannerInfo = null, isProduction = false, - resumableClaimId = null + resumableClaimId = null, ), ) } @@ -494,8 +485,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -512,8 +502,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -544,8 +533,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -562,8 +550,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null - + resumableClaimId = null, ), ) } @@ -590,8 +577,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList()), travelBannerInfo = null, - resumableClaimId = null - + resumableClaimId = null, ) } diff --git a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/ClaimStatusCards.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/ClaimStatusCards.kt index b363437892..7e2a3e520b 100644 --- a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/ClaimStatusCards.kt +++ b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/ClaimStatusCards.kt @@ -20,6 +20,7 @@ import com.hedvig.android.design.system.hedvig.HedvigPreview import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.LocalContentColor import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.ui.claimstatus.model.ClaimCardUiState import com.hedvig.android.ui.claimstatus.model.ClaimPillType.Claim import com.hedvig.android.ui.claimstatus.model.ClaimPillType.Closed.NotCompensated import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment @@ -31,18 +32,22 @@ import kotlin.time.Instant @Composable fun ClaimStatusCards( onClick: (claimId: String) -> Unit, - claimStatusCardsUiState: NonEmptyList, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (draftId: String) -> Unit, + claimCardsUiState: NonEmptyList, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { - if (claimStatusCardsUiState.size == 1) { - ClaimStatusCard( - uiState = claimStatusCardsUiState.first(), + if (claimCardsUiState.size == 1) { + ClaimCard( + uiState = claimCardsUiState.first(), onClick = onClick, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, modifier = modifier.padding(contentPadding), ) } else { - val pagerState = rememberPagerState(pageCount = { claimStatusCardsUiState.size }) + val pagerState = rememberPagerState(pageCount = { claimCardsUiState.size }) Column(modifier) { HorizontalPager( state = pagerState, @@ -51,18 +56,18 @@ fun ClaimStatusCards( pageSpacing = 8.dp, modifier = Modifier.fillMaxWidth().systemGestureExclusion(), ) { page: Int -> - val claimStatusUiState = claimStatusCardsUiState[page] - ClaimStatusCard( - uiState = claimStatusUiState, + ClaimCard( + uiState = claimCardsUiState[page], onClick = onClick, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, modifier = Modifier.fillMaxWidth(), ) } Spacer(Modifier.height(16.dp)) - HorizontalPagerIndicator( pagerState = pagerState, - pageCount = claimStatusCardsUiState.size, + pageCount = claimCardsUiState.size, activeColor = LocalContentColor.current, modifier = Modifier.padding(contentPadding).align(Alignment.CenterHorizontally), ) @@ -70,6 +75,30 @@ fun ClaimStatusCards( } } +@Composable +private fun ClaimCard( + uiState: ClaimCardUiState, + onClick: (claimId: String) -> Unit, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (draftId: String) -> Unit, + modifier: Modifier = Modifier, +) { + when (uiState) { + is ClaimCardUiState.Claim -> ClaimStatusCard( + uiState = uiState.uiState, + onClick = onClick, + modifier = modifier, + ) + + is ClaimCardUiState.Draft -> DraftClaimCard( + uiState = uiState, + onContinueClick = onContinueDraftClaim, + onDeleteClick = { onDeleteDraftClaim(uiState.id) }, + modifier = modifier, + ) + } +} + @HedvigPreview @Composable private fun PreviewClaimStatusCards() { @@ -77,19 +106,36 @@ private fun PreviewClaimStatusCards() { Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { ClaimStatusCards( onClick = {}, + onContinueDraftClaim = {}, + onDeleteDraftClaim = {}, contentPadding = PaddingValues(horizontal = 16.dp), - claimStatusCardsUiState = List(3) { - ClaimStatusCardUiState( - id = "id#$it", - pillTypes = listOf(Claim, NotCompensated), - claimProgressItemsUiState = listOf( - ClaimProgressSegment(Closed, SegmentType.INACTIVE), + claimCardsUiState = listOf( + ClaimCardUiState.Draft("id", "My things", Instant.parse("2026-07-02T00:00:00Z")), + ClaimCardUiState.Claim( + ClaimStatusCardUiState( + id = "id#0", + pillTypes = listOf(Claim, NotCompensated), + claimProgressItemsUiState = listOf( + ClaimProgressSegment(Closed, SegmentType.INACTIVE), + ), + claimType = "Broken item", + insuranceDisplayName = "Home Insurance Homeowner", + submittedDate = Instant.parse("2024-05-01T00:00:00Z"), + ), + ), + ClaimCardUiState.Claim( + ClaimStatusCardUiState( + id = "id#1", + pillTypes = listOf(Claim, NotCompensated), + claimProgressItemsUiState = listOf( + ClaimProgressSegment(Closed, SegmentType.INACTIVE), + ), + claimType = "Broken item", + insuranceDisplayName = "Home Insurance Homeowner", + submittedDate = Instant.parse("2024-05-01T00:00:00Z"), ), - claimType = "Broken item", - insuranceDisplayName = "Home Insurance Homeowner", - submittedDate = Instant.parse("2024-05-01T00:00:00Z"), - ) - }.toNonEmptyListOrNull()!!, + ), + ).toNonEmptyListOrNull()!!, ) } } diff --git a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/DraftClaimCard.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/DraftClaimCard.kt new file mode 100644 index 0000000000..a8bd753027 --- /dev/null +++ b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/DraftClaimCard.kt @@ -0,0 +1,120 @@ +package com.hedvig.android.ui.claimstatus + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize.Medium +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Primary +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Secondary +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigDateTimeFormatterDefaults +import com.hedvig.android.design.system.hedvig.HedvigPreview +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HighlightLabel +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighLightSize +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightColor +import com.hedvig.android.design.system.hedvig.HighlightLabelDefaults.HighlightShade.MEDIUM +import com.hedvig.android.design.system.hedvig.Surface +import com.hedvig.android.design.system.hedvig.datepicker.getLocale +import com.hedvig.android.ui.claimstatus.internal.ClaimProgressRow +import com.hedvig.android.ui.claimstatus.model.ClaimCardUiState +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType.INACTIVE +import hedvig.resources.RESUME_CLAIM_CONTINUE_BUTTON +import hedvig.resources.RESUME_CLAIM_DELETE_BUTTON +import hedvig.resources.RESUME_CLAIM_DRAFT +import hedvig.resources.RESUME_CLAIM_FALLBACK_TITLE +import hedvig.resources.RESUME_CLAIM_STATED +import hedvig.resources.Res +import kotlin.time.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import org.jetbrains.compose.resources.stringResource + +@Composable +fun DraftClaimCard( + uiState: ClaimCardUiState.Draft, + onContinueClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + HedvigCard(modifier = modifier) { + Column(Modifier.padding(16.dp)) { + HighlightLabel( + labelText = stringResource(Res.string.RESUME_CLAIM_DRAFT), + size = HighLightSize.Small, + color = HighlightColor.Amber(MEDIUM), + ) + Spacer(Modifier.height(16.dp)) + HedvigText( + text = uiState.title ?: stringResource(Res.string.RESUME_CLAIM_FALLBACK_TITLE), + style = HedvigTheme.typography.bodySmall, + modifier = Modifier.padding(horizontal = 2.dp), + ) + val formattedDate = HedvigDateTimeFormatterDefaults + .dateMonthAndYear(getLocale()) + .format(uiState.startedAt.toLocalDateTime(TimeZone.currentSystemDefault())) + HedvigText( + text = stringResource(Res.string.RESUME_CLAIM_STATED, formattedDate), + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + modifier = Modifier.padding(horizontal = 2.dp), + ) + Spacer(Modifier.height(18.dp)) + ClaimProgressRow( + claimProgressItemsUiState = listOf( + ClaimProgressSegment(SegmentText.Started, INACTIVE), + ClaimProgressSegment(SegmentText.BeingHandled, INACTIVE), + ClaimProgressSegment(SegmentText.Closed, INACTIVE), + ), + ) + Spacer(Modifier.height(16.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), + onClick = onDeleteClick, + enabled = true, + buttonStyle = Secondary, + buttonSize = Medium, + modifier = Modifier.weight(1f), + ) + HedvigButton( + text = stringResource(Res.string.RESUME_CLAIM_CONTINUE_BUTTON), + onClick = onContinueClick, + enabled = true, + buttonStyle = Primary, + buttonSize = Medium, + modifier = Modifier.weight(1f), + ) + } + } + } +} + +@HedvigPreview +@Composable +private fun PreviewDraftClaimCard() { + HedvigTheme { + Surface(color = HedvigTheme.colorScheme.backgroundPrimary) { + DraftClaimCard( + uiState = ClaimCardUiState.Draft( + id = "id", + title = "My things", + startedAt = Instant.parse("2026-07-02T00:00:00Z"), + ), + onContinueClick = {}, + onDeleteClick = {}, + ) + } + } +} diff --git a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/internal/ClaimProgressRow.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/internal/ClaimProgressRow.kt index 36b5db2119..5356d34c97 100644 --- a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/internal/ClaimProgressRow.kt +++ b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/internal/ClaimProgressRow.kt @@ -25,6 +25,7 @@ import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText.BeingHandled import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText.Closed +import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText.Started import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentText.Submitted import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType.ACTIVE @@ -79,8 +80,13 @@ private fun ClaimProgress( } val text = when (segmentText) { Submitted -> stringResource(Res.string.claim_status_detail_submitted) + BeingHandled -> stringResource(Res.string.claim_status_bar_being_handled) + Closed -> stringResource(Res.string.claim_status_detail_closed) + + // TODO: Add "Started" / "Påbörjad" to Lokalise + Started -> "Started" } ClaimProgress( text = text, diff --git a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimCardUiState.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimCardUiState.kt new file mode 100644 index 0000000000..8433f88dd9 --- /dev/null +++ b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimCardUiState.kt @@ -0,0 +1,13 @@ +package com.hedvig.android.ui.claimstatus.model + +import kotlin.time.Instant + +sealed interface ClaimCardUiState { + data class Claim(val uiState: ClaimStatusCardUiState) : ClaimCardUiState + + data class Draft( + val id: String, + val title: String?, + val startedAt: Instant, + ) : ClaimCardUiState +} diff --git a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt index 71dc5400f0..ca879b287a 100644 --- a/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt +++ b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt @@ -13,6 +13,7 @@ data class ClaimProgressSegment( Submitted, BeingHandled, Closed, + Started, } enum class SegmentType { From 34ccfa5861932fbeac93b3503caa20a06164ad4d Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:03:22 +0200 Subject: [PATCH 17/28] Replace ClaimChatKey resumableClaimId with resumeClaim flag --- .../app/navigation/HedvigEntryProvider.kt | 18 ++++++++++-------- .../feature/chat/inbox/InboxDestination.kt | 6 +++--- .../feature/chat/navigation/CbmChatEntries.kt | 2 +- .../claim/chat/navigation/ClaimChatEntries.kt | 4 ++-- .../feature/claim/chat/ClaimChatViewModel.kt | 8 ++++---- .../claim/chat/ui/ClaimChatDestination.kt | 6 +++--- .../home/home/navigation/HomeEntries.kt | 2 +- .../feature/home/home/ui/HomeDestination.kt | 6 +++--- 8 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt index 7b6f04cc6e..77d48d8a39 100644 --- a/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt +++ b/app/app/src/main/kotlin/com/hedvig/android/app/navigation/HedvigEntryProvider.kt @@ -230,12 +230,14 @@ private fun EntryProviderScope.addHomeEntries( backstack.add(CoInsuredAddInfoKey(contractId, type)) }, navigateToHelpCenter = { backstack.add(HelpCenterKey) }, - navigateToClaimChat = { resumableClaimId -> - backstack.add(ClaimChatKey( - messageId = null, - isDevelopmentFlow = false, - resumableClaimId = resumableClaimId, - )) + navigateToClaimChat = { resumeClaim -> + backstack.add( + ClaimChatKey( + messageId = null, + isDevelopmentFlow = false, + resumeClaim = resumeClaim, + ), + ) }, navigateToChipIdScreen = { backstack.add(ChipIdKey()) }, openAppSettings = externalNavigator::openAppSettings, @@ -475,8 +477,8 @@ private fun EntryProviderScope.addChatEntries( }, onNavigateToImageViewer = onNavigateToImageViewer, onNavigateToNewConversation = navigateToNewConversation, - navigateToClaimChat = { - backstack.add(ClaimChatKey(messageId = null, isDevelopmentFlow = false)) + navigateToClaimChat = { resumeClaim -> + backstack.add(ClaimChatKey(messageId = null, isDevelopmentFlow = false, resumeClaim = resumeClaim)) }, backstack = backstack, ) diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt index 390005cba1..f2a8e53fa3 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt @@ -108,7 +108,7 @@ internal fun InboxDestination( navigateUp: () -> Unit, onConversationClick: (id: String) -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() InboxScreen( @@ -128,7 +128,7 @@ private fun InboxScreen( onConversationClick: (id: String) -> Unit, onNavigateToNewConversation: () -> Unit, reload: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, ) { val newChatSelectBottomSheetState = rememberHedvigBottomSheetState() val startClaimBottomSheetState = rememberHedvigBottomSheetState() @@ -154,7 +154,7 @@ private fun InboxScreen( state = startClaimBottomSheetState, navigateToClaimChat = { startClaimBottomSheetState.dismiss() - navigateToClaimChat() + navigateToClaimChat(false) }, ) Surface( diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/navigation/CbmChatEntries.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/navigation/CbmChatEntries.kt index 954b37d793..d9395543f7 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/navigation/CbmChatEntries.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/navigation/CbmChatEntries.kt @@ -22,7 +22,7 @@ fun EntryProviderScope.cbmChatEntries( onNavigateToClaimDetails: (claimId: String) -> Unit, onNavigateToImageViewer: (imageUrl: String, cacheKey: String) -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: () -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, backstack: Backstack, ) { entry { diff --git a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt index efa88f89da..eec39bc248 100644 --- a/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt +++ b/app/feature/feature-claim-chat/src/androidMain/kotlin/com/hedvig/feature/claim/chat/navigation/ClaimChatEntries.kt @@ -21,7 +21,7 @@ import kotlinx.serialization.Serializable data class ClaimChatKey( val isDevelopmentFlow: Boolean = false, val messageId: String? = null, - val resumableClaimId: String? = null, + val resumeClaim: Boolean = false, ) : HedvigNavKey @Serializable @@ -59,7 +59,7 @@ fun EntryProviderScope.claimChatEntries( ) { entry { key -> ClaimChatDestination( - resumableClaimId = key.resumableClaimId, + resumeClaim = key.resumeClaim, isDevelopmentFlow = key.isDevelopmentFlow, shouldShowRequestPermissionRationale = shouldShowRequestPermissionRationale, openAppSettings = openAppSettings, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index 9740ad56ea..4437fe0598 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -164,7 +164,7 @@ internal sealed interface ClaimChatUiState { @HedvigViewModel(ActivityRetainedScope::class) internal class ClaimChatViewModel( @Assisted developmentFlow: Boolean, - @Assisted resumableClaimId: String?, + @Assisted resumeClaim: Boolean, startClaimIntentUseCase: StartClaimIntentUseCase, getClaimIntentUseCase: GetClaimIntentUseCase, submitTaskUseCase: SubmitTaskUseCase, @@ -196,7 +196,7 @@ internal class ClaimChatViewModel( fileService, regretStepUseCase, formFieldSearchUseCase, - resumableClaimId, + resumeClaim, resumeClaimUseCase, ), ) { @@ -221,7 +221,7 @@ internal class ClaimChatPresenter( private val fileService: FileService, private val regretStepUseCase: RegretStepUseCase, private val formFieldSearchUseCase: FormFieldSearchUseCase, - private val resumableClaimId: String?, + private val resumeClaim: Boolean, private val resumeClaimUseCase: ResumeClaimUseCase, ) : MoleculePresenter { @Composable @@ -261,7 +261,7 @@ internal class ClaimChatPresenter( if (initializing) { LaunchedEffect(Unit) { - val isResumingClaim = resumableClaimId != null + val isResumingClaim = resumeClaim if (isResumingClaim) { resumeClaimUseCase .invoke() diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index 6c3d602e1a..22d13174ce 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -137,11 +137,11 @@ internal fun ClaimChatDestination( isDevelopmentFlow: Boolean, navigateUp: () -> Unit, openPlayStore: () -> Unit, - resumableClaimId: String?, + resumeClaim: Boolean, ) { val claimChatViewModel = assistedMetroViewModel { - create(isDevelopmentFlow, resumableClaimId) + create(isDevelopmentFlow, resumeClaim) } Box(Modifier.fillMaxSize(), propagateMinConstraints = true) { BlurredGradientBackground() @@ -324,7 +324,7 @@ private fun ClaimChatScreenContent( HedvigAlertDialog( title = stringResource(Res.string.GENERAL_ARE_YOU_SURE), // text = stringResource(Res.string.claims_alert_body), - text = "Your answers will be saved in a draft claim", //TODO + text = "Your answers will be saved in a draft claim", // TODO onDismissRequest = { showCloseFlowDialog = false }, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index 885cc4c4f9..aef2e78ce9 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -25,7 +25,7 @@ fun EntryProviderScope.homeEntries( navigateToContactInfo: () -> Unit, navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToHelpCenter: () -> Unit, - navigateToClaimChat: (String?) -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, navigateToChipIdScreen: () -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 14f901fc0f..8d1d1b0d0d 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -157,7 +157,7 @@ internal fun HomeDestination( viewModel: HomeViewModel, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: (String?) -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, onClaimDetailCardClicked: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, @@ -207,7 +207,7 @@ private fun HomeScreen( reload: () -> Unit, onNavigateToInbox: () -> Unit, onNavigateToNewConversation: () -> Unit, - navigateToClaimChat: (String?) -> Unit, + navigateToClaimChat: (resumeClaim: Boolean) -> Unit, onClaimDetailCardClicked: (claimId: String) -> Unit, navigateToConnectPayment: () -> Unit, navigateToConnectPayout: () -> Unit, @@ -244,7 +244,7 @@ private fun HomeScreen( StartClaimBottomSheet( state = startClaimBottomSheetState, navigateToClaimChat = { - navigateToClaimChat(null) + navigateToClaimChat(false) }, ) Box(Modifier.fillMaxSize()) { From 61fd319c4b164b0f555d090c62afc866a6e10f82 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:15:31 +0200 Subject: [PATCH 18/28] Carry flag-gated DraftClaim through the home data layer --- .../src/main/graphql/QueryHome.graphql | 6 +- .../home/home/data/GetHomeDataUseCase.kt | 256 ++++++++++-------- .../home/home/data/GetHomeDataUseCaseDemo.kt | 2 +- .../feature/home/home/ui/HomeDestination.kt | 4 +- .../feature/home/home/ui/HomePresenter.kt | 10 +- .../home/home/data/GetHomeUseCaseTest.kt | 29 +- .../feature/home/home/ui/HomePresenterTest.kt | 32 +-- 7 files changed, 187 insertions(+), 152 deletions(-) diff --git a/app/feature/feature-home/src/main/graphql/QueryHome.graphql b/app/feature/feature-home/src/main/graphql/QueryHome.graphql index c971555513..eb2fa3e23f 100644 --- a/app/feature/feature-home/src/main/graphql/QueryHome.graphql +++ b/app/feature/feature-home/src/main/graphql/QueryHome.graphql @@ -1,7 +1,9 @@ -query Home($claimsHistoryFlag: Boolean!) { +query Home($claimsHistoryFlag: Boolean!, $resumeClaimEnabled: Boolean!) { currentMember { - resumableClaimIntent { + resumableClaimIntent @include(if: $resumeClaimEnabled) { id + displayName + createdAt } claims@skip(if: $claimsHistoryFlag) { ...ClaimFragment diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt index d5b9335946..e3d04b2bfe 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCase.kt @@ -31,12 +31,16 @@ import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState import com.hedvig.android.ui.emergency.FirstVetSection import dev.zacsweers.metro.Inject import kotlin.time.Clock +import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds +import kotlin.time.Instant +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.isActive import kotlinx.datetime.LocalDate @@ -60,124 +64,138 @@ internal class GetHomeDataUseCaseImpl( private val getAddonBannerInfoUseCase: GetAddonBannerInfoUseCase, private val hasAnyActiveConversationUseCase: HasAnyActiveConversationUseCase, ) : GetHomeDataUseCase { + @OptIn(ExperimentalCoroutinesApi::class) override fun invoke(forceNetworkFetch: Boolean): Flow> { - return combine( - apolloClient.query(HomeQuery(true)) - .fetchPolicy(if (forceNetworkFetch) FetchPolicy.NetworkOnly else FetchPolicy.CacheAndNetwork) - .safeFlow(), - flow { - while (currentCoroutineContext().isActive) { - emitAll( - apolloClient.query(UnreadMessageCountQuery()) - .fetchPolicy(FetchPolicy.CacheAndNetwork) - .safeFlow(), - ) - delay(5.seconds) - } - }, - getMemberRemindersUseCase.invoke(), - flow { - emitAll(getAddonBannerInfoUseCase.invoke(AddonBannerSource.INSURANCES_TAB)) - }, - featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), - hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), - ) { - homeQueryDataResult, - unreadMessageCountResult, - memberReminders, - travelBannerInfo, - inboxAlwaysAvailable, - anyActiveConversations, - -> - either { - val homeQueryData: HomeQuery.Data = homeQueryDataResult.bind() - val contractStatus = homeQueryData.currentMember.toContractStatus() - val veryImportantMessages = homeQueryData.currentMember.importantMessages.map { - HomeData.VeryImportantMessage( - id = it.id, - message = it.message, - linkInfo = it.linkInfo?.let { linkInfo -> - if (linkInfo.url.isEmpty()) { - logcat(LogPriority.ERROR) { "Backend should never return a present linkInfo with an empty url string" } - null + return featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME) + .flatMapLatest { resumeClaimEnabled -> + combine( + apolloClient.query(HomeQuery(true, resumeClaimEnabled)) + .fetchPolicy(if (forceNetworkFetch) FetchPolicy.NetworkOnly else FetchPolicy.CacheAndNetwork) + .safeFlow(), + flow { + while (currentCoroutineContext().isActive) { + emitAll( + apolloClient.query(UnreadMessageCountQuery()) + .fetchPolicy(FetchPolicy.CacheAndNetwork) + .safeFlow(), + ) + delay(5.seconds) + } + }, + getMemberRemindersUseCase.invoke(), + flow { + emitAll(getAddonBannerInfoUseCase.invoke(AddonBannerSource.INSURANCES_TAB)) + }, + featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), + hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), + ) { + homeQueryDataResult, + unreadMessageCountResult, + memberReminders, + travelBannerInfo, + inboxAlwaysAvailable, + anyActiveConversations, + -> + either { + val homeQueryData: HomeQuery.Data = homeQueryDataResult.bind() + val contractStatus = homeQueryData.currentMember.toContractStatus() + val veryImportantMessages = homeQueryData.currentMember.importantMessages.map { + HomeData.VeryImportantMessage( + id = it.id, + message = it.message, + linkInfo = it.linkInfo?.let { linkInfo -> + if (linkInfo.url.isEmpty()) { + logcat(LogPriority.ERROR) { + "Backend should never return a present linkInfo with an empty url string" + } + null + } else { + val buttonText = linkInfo.buttonText.takeIf { it.isNotEmpty() } + if (buttonText == null) { + logcat(LogPriority.ERROR) { + "Backend should never return a present buttonText with an empty string" + } + } + HomeData.VeryImportantMessage.LinkInfo( + buttonText = buttonText, + link = linkInfo.url, + ) + } + }, + ) + } + val crossSellsData = homeQueryData.currentMember.crossSellV2 + + val recommendedCrossSell = crossSellsData.recommendedCrossSell?.let { + val bundleProgress = if (it.numberOfEligibleContracts > 0 && it.discountPercent != null) { + BundleProgress(it.numberOfEligibleContracts, it.discountPercent) } else { - val buttonText = linkInfo.buttonText.takeIf { it.isNotEmpty() } - if (buttonText == null) { - logcat(LogPriority.ERROR) { "Backend should never return a present buttonText with an empty string" } - } - HomeData.VeryImportantMessage.LinkInfo( - buttonText = buttonText, - link = linkInfo.url, - ) + null } - }, - ) - } - val crossSellsData = homeQueryData.currentMember.crossSellV2 - - val recommendedCrossSell = crossSellsData.recommendedCrossSell?.let { - val bundleProgress = if (it.numberOfEligibleContracts > 0 && it.discountPercent != null) { - BundleProgress(it.numberOfEligibleContracts, it.discountPercent) - } else { - null + RecommendedCrossSell( + crossSell = it.crossSell.toCrossSell(), + bannerText = it.bannerText, + buttonText = it.buttonText, + discountText = it.discountText, + buttonDescription = it.buttonDescription, + bundleProgress = bundleProgress, + backgroundPillowImages = it.backgroundPillowImages?.let { images -> + images.leftImage.src to images.rightImage.src + }, + ) + } + val otherCrossSellsData = crossSellsData.otherCrossSells.map { + it.toCrossSell() + } + val crossSells = CrossSellSheetData( + recommendedCrossSell = recommendedCrossSell, + otherCrossSells = otherCrossSellsData, + ) + val showChatIcon = shouldShowChatButton( + isInboxEnabledFromKillSwitch = inboxAlwaysAvailable, + hasActiveConversations = anyActiveConversations.bind(), + ) + val unreadMessageCountData = unreadMessageCountResult.bind() + val hasUnseenChatMessages = unreadMessageCountData + .currentMember + .conversations + .map { it.unreadMessageCount } + .plus(unreadMessageCountData.currentMember.legacyConversation?.unreadMessageCount) + .any { it != null && it > 0 } + val firstVetActions = homeQueryData.currentMember.memberActions + ?.firstVetAction?.sections?.map { section -> + FirstVetSection( + section.buttonTitle, + section.description, + section.title, + section.url, + ) + } ?: emptyList() + val travelBannerInfo = travelBannerInfo.getOrNull() + HomeData( + contractStatus = contractStatus, + claimStatusCardsData = homeQueryData.claimStatusCards(), + veryImportantMessages = veryImportantMessages, + memberReminders = memberReminders, + hasUnseenChatMessages = hasUnseenChatMessages, + showHelpCenter = true, + firstVetSections = firstVetActions, + crossSells = crossSells, + travelBannerInfo = travelBannerInfo?.firstOrNull(), + showChatIcon = showChatIcon, + draftClaim = homeQueryData.currentMember.resumableClaimIntent?.let { resumableClaimIntent -> + HomeData.DraftClaim( + id = resumableClaimIntent.id, + displayName = resumableClaimIntent.displayName, + startedAt = resumableClaimIntent.createdAt, + ) + }, + ) + }.onLeft { error: ApolloOperationError -> + logcat(operationError = error) { "GetHomeDataUseCase failed with $error" } } - RecommendedCrossSell( - crossSell = it.crossSell.toCrossSell(), - bannerText = it.bannerText, - buttonText = it.buttonText, - discountText = it.discountText, - buttonDescription = it.buttonDescription, - bundleProgress = bundleProgress, - backgroundPillowImages = it.backgroundPillowImages?.let { images -> - images.leftImage.src to images.rightImage.src - }, - ) - } - val otherCrossSellsData = crossSellsData.otherCrossSells.map { - it.toCrossSell() } - val crossSells = CrossSellSheetData( - recommendedCrossSell = recommendedCrossSell, - otherCrossSells = otherCrossSellsData, - ) - val showChatIcon = shouldShowChatButton( - isInboxEnabledFromKillSwitch = inboxAlwaysAvailable, - hasActiveConversations = anyActiveConversations.bind(), - ) - val unreadMessageCountData = unreadMessageCountResult.bind() - val hasUnseenChatMessages = unreadMessageCountData - .currentMember - .conversations - .map { it.unreadMessageCount } - .plus(unreadMessageCountData.currentMember.legacyConversation?.unreadMessageCount) - .any { it != null && it > 0 } - val firstVetActions = homeQueryData.currentMember.memberActions - ?.firstVetAction?.sections?.map { section -> - FirstVetSection( - section.buttonTitle, - section.description, - section.title, - section.url, - ) - } ?: emptyList() - val travelBannerInfo = travelBannerInfo.getOrNull() - HomeData( - contractStatus = contractStatus, - claimStatusCardsData = homeQueryData.claimStatusCards(), - veryImportantMessages = veryImportantMessages, - memberReminders = memberReminders, - hasUnseenChatMessages = hasUnseenChatMessages, - showHelpCenter = true, - firstVetSections = firstVetActions, - crossSells = crossSells, - travelBannerInfo = travelBannerInfo?.firstOrNull(), - showChatIcon = showChatIcon, - resumableClaimId = homeQueryData.currentMember.resumableClaimIntent?.id, - ) - }.onLeft { error: ApolloOperationError -> - logcat(operationError = error) { "GetHomeDataUseCase failed with $error" } } - } } private fun shouldShowChatButton(isInboxEnabledFromKillSwitch: Boolean, hasActiveConversations: Boolean): Boolean { @@ -281,7 +299,7 @@ data class HomeData( val firstVetSections: List, val crossSells: CrossSellSheetData, val travelBannerInfo: AddonBannerInfo?, - val resumableClaimId: String?, + val draftClaim: DraftClaim?, ) { @Immutable data class ClaimStatusCardsData( @@ -299,6 +317,18 @@ data class HomeData( ) } + data class DraftClaim( + val id: String, + val displayName: String?, + val startedAt: Instant, + ) { + /** + * Drafts are kept for 7 days on the backend ("Your claim is automatically saved for 7 days"). + * Client-side heuristic, same as iOS. + */ + fun isExpired(now: Instant): Boolean = now > startedAt + 7.days + } + sealed interface ContractStatus { data object Active : ContractStatus diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt index f40fc3da7c..24d3a0eb39 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/data/GetHomeDataUseCaseDemo.kt @@ -56,7 +56,7 @@ internal class GetHomeDataUseCaseDemo : GetHomeDataUseCase { ), travelBannerInfo = null, showChatIcon = false, - resumableClaimId = null, + draftClaim = null, ).right(), ) } diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 8d1d1b0d0d..87713eb2d7 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -808,7 +808,7 @@ private fun PreviewHomeScreen( flowType = FlowType.APP_TRAVEL_PLUS_SELL_OR_UPGRADE, ), isProduction = true, - resumableClaimId = null, + draftClaim = null, ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, @@ -894,7 +894,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( chatAction = ChatAction, addonBannerInfo = null, isProduction = true, - resumableClaimId = null, + draftClaim = null, ), notificationPermissionState = rememberPreviewNotificationPermissionState(), reload = {}, diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 5f97b02d94..9f642252c3 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -138,7 +138,7 @@ internal class HomePresenter( crossSellsAction = successData.crossSellsAction, addonBannerInfo = successData.addonBannerInfo, isProduction = isProduction, - resumableClaimId = successData.resumableClaimId, + draftClaim = successData.draftClaim, ) } } @@ -178,7 +178,7 @@ internal sealed interface HomeUiState { val isProduction: Boolean, override val isHelpCenterEnabled: Boolean, override val hasUnseenChatMessages: Boolean, - val resumableClaimId: String?, + val draftClaim: HomeData.DraftClaim?, ) : HomeUiState data class Error(val message: String?) : HomeUiState @@ -197,7 +197,7 @@ private data class SuccessData( val crossSellsAction: HomeTopBarAction.CrossSellsAction?, val hasUnseenChatMessages: Boolean, val addonBannerInfo: AddonBannerInfo?, - val resumableClaimId: String?, + val draftClaim: HomeData.DraftClaim?, ) { companion object { fun fromLastState(lastState: HomeUiState): SuccessData? { @@ -213,7 +213,7 @@ private data class SuccessData( hasUnseenChatMessages = lastState.hasUnseenChatMessages, addonBannerInfo = lastState.addonBannerInfo, chatAction = lastState.chatAction, - resumableClaimId = lastState.resumableClaimId, + draftClaim = lastState.draftClaim, ) } @@ -261,7 +261,7 @@ private data class SuccessData( hasUnseenChatMessages = homeData.hasUnseenChatMessages, addonBannerInfo = homeData.travelBannerInfo, chatAction = if (homeData.showChatIcon) HomeTopBarAction.ChatAction else null, - resumableClaimId = homeData.resumableClaimId, + draftClaim = homeData.draftClaim, ) } } diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt index efd28833a0..cc6c89121d 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt @@ -91,7 +91,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = GetHomeDataUseCaseImpl( apolloClient.apply { registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( @@ -140,7 +140,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = GetHomeDataUseCaseImpl( apolloClient.apply { registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( @@ -175,7 +175,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { importantMessages = List(3) { index -> @@ -217,7 +217,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { importantMessages = emptyList() @@ -246,7 +246,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { claimsActive = listOf( @@ -288,7 +288,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { claimsActive = emptyList() @@ -317,7 +317,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { activeContracts = emptyList() @@ -364,7 +364,7 @@ internal class GetHomeUseCaseTest { testClock.now().toLocalDateTime(timeZone).date } apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolverWithFilledLists) { currentMember = buildMember { activeContracts = listOf( @@ -406,7 +406,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { activeContracts = emptyList() @@ -452,12 +452,13 @@ internal class GetHomeUseCaseTest { mapOf( // With the inbox-always-available kill switch off, the icon depends purely on existing conversations Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to false, + Feature.ENABLE_CLAIM_INTENT_RESUME to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, false), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( @@ -514,12 +515,13 @@ internal class GetHomeUseCaseTest { val featureManager = FakeFeatureManager( mapOf( Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to inboxAlwaysAvailable, + Feature.ENABLE_CLAIM_INTENT_RESUME to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, false), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( @@ -574,12 +576,13 @@ internal class GetHomeUseCaseTest { mapOf( // Inbox-always-available off, so the icon reflects the conversation state being tested here Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX to false, + Feature.ENABLE_CLAIM_INTENT_RESUME to false, ), ) val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, false), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( @@ -631,7 +634,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders(featureManager) apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver), ) apolloClient.registerTestResponse( diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 620c01199a..78523ed052 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -143,7 +143,7 @@ internal class HomePresenterTest { crossSells = CrossSellSheetData(testCrossSell, listOf()), firstVetSections = listOf(), travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -175,7 +175,7 @@ internal class HomePresenterTest { hasUnseenChatMessages = false, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -209,7 +209,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -228,7 +228,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -286,7 +286,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, listOf()), travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()) @@ -322,7 +322,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -339,7 +339,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -378,7 +378,7 @@ internal class HomePresenterTest { ), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -395,7 +395,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -433,7 +433,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -454,7 +454,7 @@ internal class HomePresenterTest { ), addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -485,7 +485,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -502,7 +502,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -533,7 +533,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()).isEqualTo( @@ -550,7 +550,7 @@ internal class HomePresenterTest { crossSellsAction = null, addonBannerInfo = null, isProduction = false, - resumableClaimId = null, + draftClaim = null, ), ) } @@ -577,7 +577,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), crossSells = CrossSellSheetData(null, emptyList()), travelBannerInfo = null, - resumableClaimId = null, + draftClaim = null, ) } From c12eecf5a824dde7383f13b31f3c07421d930aa1 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:25:53 +0200 Subject: [PATCH 19/28] Add delete-draft-claim event to the home presenter --- app/feature/feature-home/build.gradle.kts | 1 + .../feature/home/home/ui/HomePresenter.kt | 15 ++++ .../feature/home/home/ui/HomeViewModel.kt | 3 + .../feature/home/home/ui/HomePresenterTest.kt | 82 +++++++++++++++++++ 4 files changed, 101 insertions(+) diff --git a/app/feature/feature-home/build.gradle.kts b/app/feature/feature-home/build.gradle.kts index e739b6f13a..c86627faa7 100644 --- a/app/feature/feature-home/build.gradle.kts +++ b/app/feature/feature-home/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.coreResources) implementation(projects.crossSells) implementation(projects.dataAddons) + implementation(projects.dataClaimIntent) implementation(projects.dataContract) implementation(projects.dataConversations) implementation(projects.dataCrossSellAfterClaimClosed) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt index 9f642252c3..c5a4740a51 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenter.kt @@ -14,9 +14,12 @@ import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.core.common.ApplicationScope import com.hedvig.android.crosssells.CrossSellSheetData import com.hedvig.android.data.addons.data.AddonBannerInfo +import com.hedvig.android.data.claimintent.DeleteClaimIntentDraftUseCase import com.hedvig.android.feature.home.home.data.GetHomeDataUseCase import com.hedvig.android.feature.home.home.data.HomeData import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorage +import com.hedvig.android.logger.LogPriority +import com.hedvig.android.logger.logcat import com.hedvig.android.memberreminders.MemberReminders import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope @@ -36,6 +39,7 @@ internal class HomePresenter( private val crossSellHomeNotificationService: CrossSellHomeNotificationService, private val applicationScope: ApplicationScope, private val isProduction: Boolean, + private val deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: HomeUiState): HomeUiState { @@ -66,6 +70,15 @@ internal class HomePresenter( is HomeEvent.CrossSellToolTipShown -> { crossSellToolTipShownEpochDay = homeEvent.epochDay } + + is HomeEvent.DeleteDraftClaim -> { + launch { + deleteClaimIntentDraftUseCase.invoke(homeEvent.draftId).fold( + ifLeft = { logcat(LogPriority.ERROR) { "Failed to delete draft claim: $it" } }, + ifRight = { loadIteration++ }, + ) + } + } } } LaunchedEffect(crossSellToolTipShownEpochDay) { @@ -153,6 +166,8 @@ internal sealed interface HomeEvent { data object MarkCardCrossSellsAsSeen : HomeEvent data class CrossSellToolTipShown(val epochDay: Long) : HomeEvent + + data class DeleteDraftClaim(val draftId: String) : HomeEvent } internal sealed interface HomeUiState { diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt index 49654c4e97..92584d1d60 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeViewModel.kt @@ -4,6 +4,7 @@ import com.hedvig.android.core.buildconstants.HedvigBuildConstants import com.hedvig.android.core.common.ApplicationScope import com.hedvig.android.core.common.di.ActivityRetainedScope import com.hedvig.android.core.common.di.HedvigViewModel +import com.hedvig.android.data.claimintent.DeleteClaimIntentDraftUseCase import com.hedvig.android.feature.home.home.data.GetHomeDataUseCase import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorage import com.hedvig.android.molecule.public.MoleculeViewModel @@ -18,6 +19,7 @@ internal class HomeViewModel( crossSellHomeNotificationService: CrossSellHomeNotificationService, applicationScope: ApplicationScope, hedvigBuildConstants: HedvigBuildConstants, + deleteClaimIntentDraftUseCase: DeleteClaimIntentDraftUseCase, ) : MoleculeViewModel( HomeUiState.Loading, HomePresenter( @@ -26,5 +28,6 @@ internal class HomeViewModel( crossSellHomeNotificationService, applicationScope, hedvigBuildConstants.isProduction, + deleteClaimIntentDraftUseCase, ), ) diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt index 78523ed052..594cdedee8 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/ui/HomePresenterTest.kt @@ -15,13 +15,16 @@ import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector import com.hedvig.android.apollo.ApolloOperationError import com.hedvig.android.core.common.ApplicationScope +import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.crosssells.CrossSellSheetData import com.hedvig.android.crosssells.RecommendedCrossSell +import com.hedvig.android.data.claimintent.DeleteClaimIntentDraftUseCase import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset import com.hedvig.android.feature.home.home.data.GetHomeDataUseCase import com.hedvig.android.feature.home.home.data.HomeData import com.hedvig.android.feature.home.home.data.SeenImportantMessagesStorageImpl +import com.hedvig.android.logger.TestLogcatLoggingRule import com.hedvig.android.memberreminders.MemberReminder import com.hedvig.android.memberreminders.MemberReminders import com.hedvig.android.molecule.test.test @@ -33,11 +36,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(TestParameterInjector::class) internal class HomePresenterTest { + @get:Rule + val testLogcatLogger = TestLogcatLoggingRule() val testCrossSell = RecommendedCrossSell( crossSell = CrossSell( "id", @@ -63,6 +69,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -90,6 +97,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -115,6 +123,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -190,6 +199,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -243,6 +253,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -267,6 +278,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -305,6 +317,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { @@ -354,6 +367,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) val firstVet = FirstVetSection( buttonTitle = "ButtonTitle", @@ -410,6 +424,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) val crossSell = CrossSell( id = "id", @@ -469,6 +484,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -517,6 +533,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) homePresenter.test(HomeUiState.Loading) { assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) @@ -556,6 +573,61 @@ internal class HomePresenterTest { } } + @Test + fun `deleting the draft claim calls the use case and reloads home on success`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val deleteClaimIntentDraftUseCase = TestDeleteClaimIntentDraftUseCase() + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + deleteClaimIntentDraftUseCase, + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isFalse() + getHomeDataUseCase.responseTurbine.add( + someIrrelevantHomeDataInstance.copy( + draftClaim = HomeData.DraftClaim("draft-id", "My things", Instant.parse("2026-07-01T00:00:00Z")), + ).right(), + ) + assertThat(awaitItem()).isInstanceOf() + + sendEvent(HomeEvent.DeleteDraftClaim("draft-id")) + assertThat(deleteClaimIntentDraftUseCase.deletedIdsTurbine.awaitItem()).isEqualTo("draft-id") + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isTrue() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `a failed draft deletion does not reload home`() = runTest { + val getHomeDataUseCase = TestGetHomeDataUseCase() + val deleteClaimIntentDraftUseCase = TestDeleteClaimIntentDraftUseCase().apply { + result = ErrorMessage().left() + } + val homePresenter = HomePresenter( + getHomeDataUseCase, + SeenImportantMessagesStorageImpl(), + FakeCrossSellHomeNotificationService(), + ApplicationScope(backgroundScope), + false, + deleteClaimIntentDraftUseCase, + ) + homePresenter.test(HomeUiState.Loading) { + assertThat(awaitItem()).isEqualTo(HomeUiState.Loading) + assertThat(getHomeDataUseCase.forceNetworkFetchTurbine.awaitItem()).isFalse() + getHomeDataUseCase.responseTurbine.add(someIrrelevantHomeDataInstance.right()) + assertThat(awaitItem()).isInstanceOf() + + sendEvent(HomeEvent.DeleteDraftClaim("draft-id")) + assertThat(deleteClaimIntentDraftUseCase.deletedIdsTurbine.awaitItem()).isEqualTo("draft-id") + getHomeDataUseCase.forceNetworkFetchTurbine.expectNoEvents() + } + } + private class TestGetHomeDataUseCase : GetHomeDataUseCase { val forceNetworkFetchTurbine = Turbine() val responseTurbine = Turbine>() @@ -596,3 +668,13 @@ private class FakeCrossSellHomeNotificationService : CrossSellHomeNotificationSe override suspend fun setLastEpochDayNewRecommendationNotificationWasShown(epochDay: Long) { } } + +private class TestDeleteClaimIntentDraftUseCase : DeleteClaimIntentDraftUseCase { + val deletedIdsTurbine = Turbine() + var result: Either = Unit.right() + + override suspend fun invoke(id: String): Either { + deletedIdsTurbine.add(id) + return result + } +} From 6dd15307207bd7f3021ff29001ca0ec2209fe822 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:33:53 +0200 Subject: [PATCH 20/28] Wire draft claim card, delete and expired dialogs on home --- .../feature/home/home/ui/HomeDestination.kt | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 87713eb2d7..617bf0ce5d 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import arrow.core.NonEmptyList import arrow.core.nonEmptyListOf import arrow.core.toNonEmptyListOrNull import coil3.ImageLoader @@ -75,6 +76,9 @@ import com.hedvig.android.data.coinsured.CoInsuredFlowType import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Secondary +import com.hedvig.android.design.system.hedvig.DraftClaimDialog +import com.hedvig.android.design.system.hedvig.ErrorDialog +import com.hedvig.android.design.system.hedvig.HedvigAlertDialog import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection import com.hedvig.android.design.system.hedvig.HedvigFullScreenCenterAlignedProgressDebounced @@ -129,8 +133,14 @@ import com.hedvig.android.ui.claimstatus.model.ClaimProgressSegment.SegmentType. import com.hedvig.android.ui.claimstatus.model.ClaimStatusCardUiState import com.hedvig.android.ui.emergency.FirstVetSection import hedvig.resources.CHAT_NEW_MESSAGE +import hedvig.resources.RESUME_CLAIM_DELETE_BODY +import hedvig.resources.RESUME_CLAIM_DELETE_BUTTON +import hedvig.resources.RESUME_CLAIM_DELETE_TITLE +import hedvig.resources.RESUME_CLAIM_EXPIRED_BODY +import hedvig.resources.RESUME_CLAIM_EXPIRED_TITLE import hedvig.resources.Res import hedvig.resources.TOAST_NEW_OFFER +import hedvig.resources.general_cancel_button import hedvig.resources.home_tab_active_in_future_info import hedvig.resources.home_tab_claim_button_text import hedvig.resources.home_tab_get_help @@ -189,6 +199,7 @@ internal fun HomeDestination( openAppSettings = openAppSettings, navigateToMissingInfo = navigateToMissingInfo, markMessageAsSeen = { viewModel.emit(HomeEvent.MarkMessageAsSeen(it)) }, + deleteDraftClaim = { draftId -> viewModel.emit(HomeEvent.DeleteDraftClaim(draftId)) }, navigateToFirstVet = navigateToFirstVet, markCrossSellsNotificationAsSeen = { viewModel.emit(HomeEvent.MarkCardCrossSellsAsSeen) }, navigateToContactInfo = navigateToContactInfo, @@ -215,6 +226,7 @@ private fun HomeScreen( openUrl: (String) -> Unit, openCrossSellUrl: (String) -> Unit, markMessageAsSeen: (String) -> Unit, + deleteDraftClaim: (String) -> Unit, openAppSettings: () -> Unit, navigateToMissingInfo: (String, CoInsuredFlowType) -> Unit, navigateToFirstVet: (List) -> Unit, @@ -247,6 +259,44 @@ private fun HomeScreen( navigateToClaimChat(false) }, ) + val draftClaim = (uiState as? Success)?.draftClaim + var showDraftClaimDialog by remember { mutableStateOf(false) } + var showDraftExpiredDialog by remember { mutableStateOf(false) } + var draftIdPendingDeleteConfirmation by remember { mutableStateOf(null) } + if (showDraftClaimDialog) { + DraftClaimDialog( + onDismissRequest = { showDraftClaimDialog = false }, + onContinueDraft = { + showDraftClaimDialog = false + navigateToClaimChat(true) + }, + onStartNewClaim = { + showDraftClaimDialog = false + startClaimBottomSheetState.show(Unit) + }, + ) + } + if (showDraftExpiredDialog) { + ErrorDialog( + title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), + message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), + onDismiss = { showDraftExpiredDialog = false }, + ) + } + val draftIdToDelete = draftIdPendingDeleteConfirmation + if (draftIdToDelete != null) { + HedvigAlertDialog( + title = stringResource(Res.string.RESUME_CLAIM_DELETE_TITLE), + text = stringResource(Res.string.RESUME_CLAIM_DELETE_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), + dismissButtonLabel = stringResource(Res.string.general_cancel_button), + onDismissRequest = { draftIdPendingDeleteConfirmation = null }, + onConfirmClick = { + draftIdPendingDeleteConfirmation = null + deleteDraftClaim(draftIdToDelete) + }, + ) + } Box(Modifier.fillMaxSize()) { val toolbarHeight = 64.dp val transition = updateTransition(targetState = uiState, label = "home ui state") @@ -283,8 +333,22 @@ private fun HomeScreen( navigateToConnectPayout = navigateToConnectPayout, navigateToHelpCenter = navigateToHelpCenter, openClaimFlowSheet = { - startClaimBottomSheetState.show(Unit) + if (draftClaim != null) { + showDraftClaimDialog = true + } else { + startClaimBottomSheetState.show(Unit) + } }, + onContinueDraftClaim = { + if (draftClaim != null) { + if (draftClaim.isExpired(Clock.System.now())) { + showDraftExpiredDialog = true + } else { + navigateToClaimChat(true) + } + } + }, + onDeleteDraftClaim = { draftId -> draftIdPendingDeleteConfirmation = draftId }, openAppSettings = openAppSettings, openUrl = openUrl, navigateToMissingInfo = navigateToMissingInfo, @@ -433,6 +497,8 @@ private fun HomeScreenSuccess( navigateToConnectPayout: () -> Unit, navigateToHelpCenter: () -> Unit, openClaimFlowSheet: () -> Unit, + onContinueDraftClaim: () -> Unit, + onDeleteDraftClaim: (String) -> Unit, openAppSettings: () -> Unit, openUrl: (String) -> Unit, markMessageAsSeen: (String) -> Unit, @@ -480,14 +546,17 @@ private fun HomeScreenSuccess( ) }, claimStatusCards = { - val claimCards = uiState.claimStatusCardsData?.claimStatusCardsUiState - ?.map { ClaimCardUiState.Claim(it) } - ?.toNonEmptyListOrNull() + val claimCards: NonEmptyList? = buildList { + uiState.draftClaim?.let { draftClaim -> + add(ClaimCardUiState.Draft(draftClaim.id, draftClaim.displayName, draftClaim.startedAt)) + } + uiState.claimStatusCardsData?.claimStatusCardsUiState?.forEach { add(ClaimCardUiState.Claim(it)) } + }.toNonEmptyListOrNull() if (claimCards != null) { ClaimStatusCards( onClick = onClaimDetailCardClicked, - onContinueDraftClaim = {}, - onDeleteDraftClaim = {}, + onContinueDraftClaim = onContinueDraftClaim, + onDeleteDraftClaim = onDeleteDraftClaim, claimCardsUiState = claimCards, contentPadding = PaddingValues(horizontal = 16.dp) + horizontalInsets, ) @@ -824,6 +893,7 @@ private fun PreviewHomeScreen( openAppSettings = {}, navigateToMissingInfo = { _, _ -> }, markMessageAsSeen = {}, + deleteDraftClaim = {}, navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, @@ -856,6 +926,7 @@ private fun PreviewHomeScreenWithError() { openAppSettings = {}, navigateToMissingInfo = { _, _ -> }, markMessageAsSeen = {}, + deleteDraftClaim = {}, navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, @@ -910,6 +981,7 @@ private fun PreviewHomeScreenAllHomeTextTypes( openAppSettings = {}, navigateToMissingInfo = { _, _ -> }, markMessageAsSeen = {}, + deleteDraftClaim = {}, navigateToFirstVet = {}, markCrossSellsNotificationAsSeen = {}, navigateToContactInfo = {}, From 42f9c24b0e061046c67cfb4b1acd1ed65e91d26b Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:47:43 +0200 Subject: [PATCH 21/28] Use displayName title and resumable-gated leave dialog in claim chat --- .../feature-claim-chat/build.gradle.kts | 1 + .../graphql/FragmentClaimIntent.graphql | 2 + .../feature/claim/chat/ClaimChatViewModel.kt | 60 +++++++++++++++---- .../feature/claim/chat/data/ClaimIntent.kt | 2 + .../feature/claim/chat/data/ClaimIntentExt.kt | 2 + .../claim/chat/ui/ClaimChatDestination.kt | 41 +++++++++---- 6 files changed, 82 insertions(+), 26 deletions(-) diff --git a/app/feature/feature-claim-chat/build.gradle.kts b/app/feature/feature-claim-chat/build.gradle.kts index 41413e0056..a379a3dc2b 100644 --- a/app/feature/feature-claim-chat/build.gradle.kts +++ b/app/feature/feature-claim-chat/build.gradle.kts @@ -46,6 +46,7 @@ kotlin { implementation(projects.coreResources) implementation(projects.coreUiData) implementation(projects.designSystemHedvig) + implementation(projects.featureFlags) implementation(projects.languageCore) implementation(projects.moleculePublic) implementation(projects.navigationCore) diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql index bc37b4b02e..122f6b3de4 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql @@ -1,6 +1,8 @@ fragment ClaimIntentFragment on ClaimIntent { id progress + displayName + resumable currentStep { id text diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt index 4437fe0598..9654bcc1c6 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ClaimChatViewModel.kt @@ -2,6 +2,7 @@ package com.hedvig.feature.claim.chat import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -15,6 +16,8 @@ import com.hedvig.android.core.common.di.ActivityRetainedScope import com.hedvig.android.core.common.di.HedvigViewModel import com.hedvig.android.core.fileupload.FileService import com.hedvig.android.core.uidata.UiFile +import com.hedvig.android.featureflags.FeatureManager +import com.hedvig.android.featureflags.flags.Feature import com.hedvig.android.logger.logcat import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope @@ -157,6 +160,9 @@ internal sealed interface ClaimChatUiState { val stepsWithShownAnimations: List, val progress: Float?, val searchQuery: SearchObject?, + val title: String?, + val isResumable: Boolean, + val resumeClaimEnabled: Boolean, ) : ClaimChatUiState } @@ -179,6 +185,7 @@ internal class ClaimChatViewModel( formFieldSearchUseCase: FormFieldSearchUseCase, fileService: FileService, resumeClaimUseCase: ResumeClaimUseCase, + featureManager: FeatureManager, ) : MoleculeViewModel( ClaimChatUiState.Initializing, ClaimChatPresenter( @@ -198,6 +205,7 @@ internal class ClaimChatViewModel( formFieldSearchUseCase, resumeClaim, resumeClaimUseCase, + featureManager, ), ) { override fun onCleared() { @@ -223,6 +231,7 @@ internal class ClaimChatPresenter( private val formFieldSearchUseCase: FormFieldSearchUseCase, private val resumeClaim: Boolean, private val resumeClaimUseCase: ResumeClaimUseCase, + private val featureManager: FeatureManager, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: ClaimChatUiState): ClaimChatUiState { @@ -253,6 +262,19 @@ internal class ClaimChatPresenter( ?: 0f, ) } + var title by remember { mutableStateOf((lastState as? ClaimChatUiState.ClaimChat)?.title) } + var isResumable by remember { + mutableStateOf((lastState as? ClaimChatUiState.ClaimChat)?.isResumable ?: false) + } + val updateIntentMetadata: (ClaimIntent) -> Unit = { intent -> + progress = intent.progress + // Keep the previous title when a step comes back without one, matching iOS. + title = intent.displayName ?: title + isResumable = intent.resumable + } + val resumeClaimEnabled by remember { + featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME) + }.collectAsState(initial = false) val stepsWithShownAnimations = remember { mutableStateListOf() } val setOutcome: (ClaimIntentOutcome) -> Unit = { outcome = it } @@ -285,7 +307,7 @@ internal class ClaimChatPresenter( it.stepContent !is StepContent.Task }, ) - progress = claimIntent.progress + updateIntentMetadata(claimIntent) when (val next = claimIntent.next) { is ClaimIntent.Next.Outcome -> { outcome = next.claimIntentOutcome @@ -313,7 +335,7 @@ internal class ClaimChatPresenter( failedToStart = false claimIntentId = claimIntent.id steps.clear() - progress = claimIntent.progress + updateIntentMetadata(claimIntent) when (val next = claimIntent.next) { is ClaimIntent.Next.Outcome -> { outcome = next.claimIntentOutcome @@ -350,7 +372,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) @@ -433,7 +456,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -469,7 +493,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -497,7 +522,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -638,7 +664,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -699,7 +726,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -758,7 +786,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -793,7 +822,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -897,7 +927,8 @@ internal class ClaimChatPresenter( steps, setOutcome, claimIntent, - ) { progress = it } + updateIntentMetadata, + ) }, ) } @@ -999,6 +1030,9 @@ internal class ClaimChatPresenter( stepsWithShownAnimations = stepsWithShownAnimations, progress = progress, searchQuery = searchQuery, + title = title, + isResumable = isResumable, + resumeClaimEnabled = resumeClaimEnabled, ) else -> error("") @@ -1075,10 +1109,10 @@ private fun handleNext( steps: SnapshotStateList, setOutcome: (outcome: ClaimIntentOutcome) -> Unit, intent: ClaimIntent, - setProgress: (Float?) -> Unit, + updateIntentMetadata: (ClaimIntent) -> Unit, ) { val next = intent.next - setProgress(intent.progress) + updateIntentMetadata(intent) when (next) { is ClaimIntent.Next.Outcome -> { setOutcome(next.claimIntentOutcome) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt index b36c5c229c..8d4cc351bf 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntent.kt @@ -15,6 +15,8 @@ internal data class ClaimIntent( val id: ClaimIntentId, val next: Next, val progress: Float?, + val displayName: String?, + val resumable: Boolean, val previousSteps: List, ) { sealed interface Next { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index b9e7a14aa4..bb5b50a995 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -57,6 +57,8 @@ internal fun ClaimIntentFragment.toClaimIntent(locale: CommonLocale): ClaimInten else -> error("ClaimIntentFragment contained null currentStep and null outcome") }, progress = progress?.toFloat(), + displayName = displayName, + resumable = resumable, previousSteps = previousSteps.map { it.toClaimIntentStep(locale) }, diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index 22d13174ce..373a853821 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -115,6 +115,9 @@ import hedvig.resources.EMBARK_UPDATE_APP_BODY import hedvig.resources.EMBARK_UPDATE_APP_BUTTON import hedvig.resources.GENERAL_ARE_YOU_SURE import hedvig.resources.NETWORK_ERROR_ALERT_MESSAGE +import hedvig.resources.RESUME_CLAIM_LEAVE_BODY +import hedvig.resources.RESUME_CLAIM_LEAVE_CONFIRM +import hedvig.resources.RESUME_CLAIM_LEAVE_TITLE import hedvig.resources.Res import hedvig.resources.claims_alert_body import hedvig.resources.general_cancel_button @@ -278,9 +281,13 @@ private fun ClaimChatScreenContent( ) { var showCloseFlowDialog by rememberSaveable { mutableStateOf(false) } + // Flag on: only a resumable draft warrants a leave confirmation (it will be saved). + // Flag off: legacy behavior, always confirm. + val showLeaveConfirmation = if (uiState.resumeClaimEnabled) uiState.isResumable else true + NavigationEventHandler( state = rememberNavigationEventState(NavigationEventInfo.None), - isBackEnabled = uiState.steps.size > 1, + isBackEnabled = uiState.steps.size > 1 && showLeaveConfirmation, ) { showCloseFlowDialog = true } @@ -321,15 +328,22 @@ private fun ClaimChatScreenContent( ) } if (showCloseFlowDialog) { - HedvigAlertDialog( - title = stringResource(Res.string.GENERAL_ARE_YOU_SURE), - // text = stringResource(Res.string.claims_alert_body), - text = "Your answers will be saved in a draft claim", // TODO - onDismissRequest = { - showCloseFlowDialog = false - }, - onConfirmClick = navigateUp, - ) + if (uiState.resumeClaimEnabled) { + HedvigAlertDialog( + title = stringResource(Res.string.RESUME_CLAIM_LEAVE_TITLE), + text = stringResource(Res.string.RESUME_CLAIM_LEAVE_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_LEAVE_CONFIRM), + onDismissRequest = { showCloseFlowDialog = false }, + onConfirmClick = navigateUp, + ) + } else { + HedvigAlertDialog( + title = stringResource(Res.string.GENERAL_ARE_YOU_SURE), + text = stringResource(Res.string.claims_alert_body), + onDismissRequest = { showCloseFlowDialog = false }, + onConfirmClick = navigateUp, + ) + } } val lazyListState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() @@ -356,12 +370,13 @@ private fun ClaimChatScreenContent( Box(modifier = modifier.fillMaxSize()) { Column(Modifier.matchParentSize()) { - val title = stringResource(Res.string.CHAT_CONVERSATION_CLAIM_TITLE) + val legacyTitle = stringResource(Res.string.CHAT_CONVERSATION_CLAIM_TITLE) + val title = if (uiState.resumeClaimEnabled) uiState.title ?: legacyTitle else legacyTitle TopAppBar( title = title, actionType = TopAppBarActionType.BACK, onActionClick = { - if (uiState.steps.size > 1) { + if (uiState.steps.size > 1 && showLeaveConfirmation) { showCloseFlowDialog = true } else { navigateUp() @@ -405,7 +420,7 @@ private fun ClaimChatScreenContent( openAppSettings = openAppSettings, modifier = Modifier.fillMaxSize(), closeFlow = { - if (uiState.steps.size > 1) { + if (uiState.steps.size > 1 && showLeaveConfirmation) { showCloseFlowDialog = true } else { navigateUp() From d60dde643d6402c660568999b5debcef9ab770da Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:52:05 +0200 Subject: [PATCH 22/28] Wire data-claim-intent contributions into the app graph --- app/app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 6ef0c2a186..5c11745bc3 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -167,7 +167,7 @@ dependencies { implementation(projects.dataAddons) implementation(projects.dataChangetier) implementation(projects.dataChat) - + implementation(projects.dataClaimIntent) implementation(projects.dataContract) implementation(projects.dataConversations) implementation(projects.dataCrossSellAfterClaimClosed) From 421a4f1e4ecec214aed9546ca7d5e4015baa39d2 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 19:57:52 +0200 Subject: [PATCH 23/28] Show draft claim dialog from the inbox start-claim entry --- app/feature/feature-chat/build.gradle.kts | 1 + .../feature/chat/inbox/InboxDestination.kt | 26 ++++++++++++++++++- .../feature/chat/inbox/InboxViewModel.kt | 17 +++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/app/feature/feature-chat/build.gradle.kts b/app/feature/feature-chat/build.gradle.kts index 0d9a9f08bd..99570b329b 100644 --- a/app/feature/feature-chat/build.gradle.kts +++ b/app/feature/feature-chat/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.coreMarkdown) implementation(projects.coreResources) implementation(projects.dataChat) + implementation(projects.dataClaimIntent) implementation(projects.designSystemHedvig) implementation(projects.featureFlags) implementation(projects.languageCore) diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt index f2a8e53fa3..46d9a5e385 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxDestination.kt @@ -24,6 +24,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,6 +47,7 @@ import com.hedvig.android.compose.ui.preview.TripleBooleanCollectionPreviewParam import com.hedvig.android.compose.ui.preview.TripleCase import com.hedvig.android.design.system.hedvig.ButtonDefaults import com.hedvig.android.design.system.hedvig.DividerPosition +import com.hedvig.android.design.system.hedvig.DraftClaimDialog import com.hedvig.android.design.system.hedvig.EmptyState import com.hedvig.android.design.system.hedvig.EmptyStateDefaults import com.hedvig.android.design.system.hedvig.HedvigBottomSheet @@ -132,6 +136,20 @@ private fun InboxScreen( ) { val newChatSelectBottomSheetState = rememberHedvigBottomSheetState() val startClaimBottomSheetState = rememberHedvigBottomSheetState() + var showDraftClaimDialog by remember { mutableStateOf(false) } + if (showDraftClaimDialog) { + DraftClaimDialog( + onDismissRequest = { showDraftClaimDialog = false }, + onContinueDraft = { + showDraftClaimDialog = false + navigateToClaimChat(true) + }, + onStartNewClaim = { + showDraftClaimDialog = false + startClaimBottomSheetState.show(Unit) + }, + ) + } HedvigBottomSheet( newChatSelectBottomSheetState, content = { @@ -142,7 +160,11 @@ private fun InboxScreen( }, onStartNewClaim = { newChatSelectBottomSheetState.dismiss() - startClaimBottomSheetState.show(Unit) + if ((uiState as? InboxUiState.Success)?.hasDraftClaim == true) { + showDraftClaimDialog = true + } else { + startClaimBottomSheetState.show(Unit) + } }, dismiss = { newChatSelectBottomSheetState.dismiss() @@ -479,6 +501,7 @@ private fun EmptyInboxSuccessScreenPreview( InboxUiState.Success( listOf(), case, + hasDraftClaim = false, ), {}, {}, @@ -518,6 +541,7 @@ private fun InboxSuccessScreenPreview( mockInboxConversationLegacy, ), newConversationButtonAvailable = case, + hasDraftClaim = false, ), {}, {}, diff --git a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt index b66564ecd5..749e24b16d 100644 --- a/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt +++ b/app/feature/feature-chat/src/main/kotlin/com/hedvig/android/feature/chat/inbox/InboxViewModel.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.hedvig.android.core.common.di.ActivityRetainedScope import com.hedvig.android.core.common.di.HedvigViewModel +import com.hedvig.android.data.claimintent.GetResumableClaimIntentUseCase import com.hedvig.android.feature.chat.data.GetAllConversationsUseCase import com.hedvig.android.feature.chat.model.InboxConversation import com.hedvig.android.featureflags.FeatureManager @@ -19,20 +20,23 @@ import com.hedvig.android.molecule.public.MoleculeViewModel import dev.zacsweers.metro.Inject import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first @Inject @HedvigViewModel(ActivityRetainedScope::class) internal class InboxViewModel( getAllConversationsUseCase: GetAllConversationsUseCase, featureManager: FeatureManager, + getResumableClaimIntentUseCase: GetResumableClaimIntentUseCase, ) : MoleculeViewModel( initialState = InboxUiState.Loading, - presenter = InboxPresenter(getAllConversationsUseCase, featureManager), + presenter = InboxPresenter(getAllConversationsUseCase, featureManager, getResumableClaimIntentUseCase), ) internal class InboxPresenter( private val getAllConversationsUseCase: GetAllConversationsUseCase, private val featureManager: FeatureManager, + private val getResumableClaimIntentUseCase: GetResumableClaimIntentUseCase, ) : MoleculePresenter { @Composable override fun MoleculePresenterScope.present(lastState: InboxUiState): InboxUiState { @@ -53,6 +57,15 @@ internal class InboxPresenter( if (currentState !is InboxUiState.Success) { currentState = InboxUiState.Loading } + val hasDraftClaim = if (featureManager.isFeatureEnabled(Feature.ENABLE_CLAIM_INTENT_RESUME).first()) { + getResumableClaimIntentUseCase.invoke().fold( + // A failed draft lookup must not block starting a claim; treat it as no draft. + ifLeft = { false }, + ifRight = { it != null }, + ) + } else { + false + } combine( getAllConversationsUseCase.invoke(), featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), @@ -69,6 +82,7 @@ internal class InboxPresenter( currentState = InboxUiState.Success( conversations, newChatButtonAvailable, + hasDraftClaim, ) }, ) @@ -86,6 +100,7 @@ internal sealed interface InboxUiState { data class Success( val inboxConversations: List, val newConversationButtonAvailable: Boolean, + val hasDraftClaim: Boolean, ) : InboxUiState } From 7a4b0f4bc305806f8e2b300c872e69f3cf3635b5 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 20:07:38 +0200 Subject: [PATCH 24/28] Clean up resume-claim leftovers --- .../com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt | 4 +++- .../com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index bb5b50a995..c0952151cd 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -125,7 +125,9 @@ private fun ClaimIntentStepContentFragment.toStepContent(locale: CommonLocale): AudioRecordingStepState.AudioRecording.Playback( audioPath = AudioPath.RemoteUrl(audioUrl), isPlaying = false, - isPrepared = true, // TODO: check + // A resumed remote recording has no local MediaPlayer to prepare; the remote audio player + // handles its own buffering, so the playback UI can show immediately. + isPrepared = true, hasError = false, ) } else if (freeText != null) { diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt index 6d6c190571..20d0423c82 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ResumeClaimUseCase.kt @@ -27,8 +27,8 @@ internal class ResumeClaimUseCase( ) .fetchPolicy(FetchPolicy.NetworkOnly) .safeExecute() - .mapLeft { - logcat { "StartClaimIntentUseCase error: $it" } + .mapLeft { error -> + logcat(operationError = error) { "ResumeClaimUseCase failed with $error" } ClaimChatErrorMessage.GeneralError } .bind() From b4778c75c66a3506fc847795ed87b7d979297d60 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 20:11:12 +0200 Subject: [PATCH 25/28] Expand wildcard imports in ClaimChatDestination --- .../claim/chat/ui/ClaimChatDestination.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt index 373a853821..11097a3019 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/ui/ClaimChatDestination.kt @@ -83,8 +83,27 @@ import com.hedvig.android.design.system.hedvig.icon.HedvigIcons import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import com.hedvig.feature.claim.chat.ClaimChatEvent -import com.hedvig.feature.claim.chat.ClaimChatEvent.* -import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.* +import com.hedvig.feature.claim.chat.ClaimChatEvent.AddToShownAnimations +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.RedoRecording +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.StartRecording +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.StopRecording +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.SubmitAudioFile +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.SubmitTextInput +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.SwitchToAudioRecording +import com.hedvig.feature.claim.chat.ClaimChatEvent.AudioRecording.SwitchToFreeText +import com.hedvig.feature.claim.chat.ClaimChatEvent.CloseFreeChatOverlay +import com.hedvig.feature.claim.chat.ClaimChatEvent.DismissConfirmEditDialog +import com.hedvig.feature.claim.chat.ClaimChatEvent.DismissErrorDialog +import com.hedvig.feature.claim.chat.ClaimChatEvent.FinishTaskAnimation +import com.hedvig.feature.claim.chat.ClaimChatEvent.HandledDeflectNavigation +import com.hedvig.feature.claim.chat.ClaimChatEvent.HandledOutcomeNavigation +import com.hedvig.feature.claim.chat.ClaimChatEvent.OpenFreeTextOverlay +import com.hedvig.feature.claim.chat.ClaimChatEvent.Regret +import com.hedvig.feature.claim.chat.ClaimChatEvent.RetryInitializing +import com.hedvig.feature.claim.chat.ClaimChatEvent.RetrySubmittingTaskStep +import com.hedvig.feature.claim.chat.ClaimChatEvent.Skip +import com.hedvig.feature.claim.chat.ClaimChatEvent.SubmitClaim +import com.hedvig.feature.claim.chat.ClaimChatEvent.UpdateFreeText import com.hedvig.feature.claim.chat.ClaimChatUiState import com.hedvig.feature.claim.chat.ClaimChatViewModel import com.hedvig.feature.claim.chat.ClaimChatViewModelFactory From 691861d70ad77f84820c730267821a9ed2b15bdd Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 3 Jul 2026 20:35:44 +0200 Subject: [PATCH 26/28] Address final review findings - Restore dropUnlessResumed guard on navigateToClaimChat in HomeEntries - Parse raw value (it.value) instead of display text (it.text) for DATE fields - Drop unmapped createdAt field from ClaimIntentFragment - Fix 5-space indent to 4-space in ResumeClaimQuery.graphql - Assert draftClaim is null when ENABLE_CLAIM_INTENT_RESUME flag is off --- .../commonMain/graphql/FragmentClaimIntent.graphql | 1 - .../src/commonMain/graphql/ResumeClaimQuery.graphql | 2 +- .../hedvig/feature/claim/chat/data/ClaimIntentExt.kt | 2 +- .../feature/home/home/navigation/HomeEntries.kt | 2 +- .../feature/home/home/data/GetHomeUseCaseTest.kt | 11 ++++++++++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql index 122f6b3de4..f9c137b0df 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/FragmentClaimIntent.graphql @@ -16,7 +16,6 @@ fragment ClaimIntentFragment on ClaimIntent { id submittedAt } - createdAt previousSteps { id text diff --git a/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql b/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql index 21231fdcd1..61e6412c55 100644 --- a/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql +++ b/app/feature/feature-claim-chat/src/commonMain/graphql/ResumeClaimQuery.graphql @@ -1,7 +1,7 @@ query ResumeClaim { currentMember { resumableClaimIntent { - ...ClaimIntentFragment + ...ClaimIntentFragment } } } diff --git a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt index c0952151cd..5eac5ccd4a 100644 --- a/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt +++ b/app/feature/feature-claim-chat/src/commonMain/kotlin/com/hedvig/feature/claim/chat/data/ClaimIntentExt.kt @@ -315,7 +315,7 @@ private fun List.toFields(locale: CommonLocale): List { DatePickerUiState( locale = locale, - initiallySelectedDate = defaultValues.getOrNull(0)?.let { LocalDate.parse(it.text) }, + initiallySelectedDate = defaultValues.getOrNull(0)?.let { LocalDate.parse(it.value) }, minDate = field.minValue?.let { LocalDate.parse(it) } ?: LocalDate(1900, 1, 1), maxDate = field.maxValue?.let { LocalDate.parse(it) } ?: LocalDate(2100, 1, 1), ) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt index aef2e78ce9..d45e66cc44 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/navigation/HomeEntries.kt @@ -38,7 +38,7 @@ fun EntryProviderScope.homeEntries( viewModel = viewModel, onNavigateToInbox = dropUnlessResumed { onNavigateToInbox() }, onNavigateToNewConversation = dropUnlessResumed { onNavigateToNewConversation() }, - navigateToClaimChat = navigateToClaimChat, + navigateToClaimChat = dropUnlessResumed { resumeClaim -> navigateToClaimChat(resumeClaim) }, onClaimDetailCardClicked = dropUnlessResumed { claimId: String -> navigateToClaimDetails(claimId) }, diff --git a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt index cc6c89121d..088ff69b79 100644 --- a/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt +++ b/app/feature/feature-home/src/test/kotlin/com/hedvig/android/feature/home/home/data/GetHomeUseCaseTest.kt @@ -459,7 +459,11 @@ internal class GetHomeUseCaseTest { apolloClient.registerTestResponse( HomeQuery(true, false), - HomeQuery.Data(OctopusFakeResolver), + HomeQuery.Data(OctopusFakeResolver) { + currentMember = buildMember { + resumableClaimIntent = null + } + }, ) apolloClient.registerTestResponse( UnreadMessageCountQuery(), @@ -505,6 +509,11 @@ internal class GetHomeUseCaseTest { isFalse() } } + assertThat(result) + .isNotNull() + .isRight() + .prop(HomeData::draftClaim) + .isNull() } @Test From 8ec5a41e6541ee6c41612a82469349ea164f2f57 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Sun, 5 Jul 2026 11:13:13 +0200 Subject: [PATCH 27/28] Delete the expired draft when starting a new claim from the expired notice --- .../feature/home/home/ui/HomeDestination.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt index 617bf0ce5d..8d42febe2d 100644 --- a/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt +++ b/app/feature/feature-home/src/main/kotlin/com/hedvig/android/feature/home/home/ui/HomeDestination.kt @@ -77,7 +77,6 @@ import com.hedvig.android.data.contract.CrossSell import com.hedvig.android.data.contract.ImageAsset import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.Secondary import com.hedvig.android.design.system.hedvig.DraftClaimDialog -import com.hedvig.android.design.system.hedvig.ErrorDialog import com.hedvig.android.design.system.hedvig.HedvigAlertDialog import com.hedvig.android.design.system.hedvig.HedvigButton import com.hedvig.android.design.system.hedvig.HedvigErrorSection @@ -136,6 +135,7 @@ import hedvig.resources.CHAT_NEW_MESSAGE import hedvig.resources.RESUME_CLAIM_DELETE_BODY import hedvig.resources.RESUME_CLAIM_DELETE_BUTTON import hedvig.resources.RESUME_CLAIM_DELETE_TITLE +import hedvig.resources.RESUME_CLAIM_DRAFT_ALERT_START_NEW import hedvig.resources.RESUME_CLAIM_EXPIRED_BODY import hedvig.resources.RESUME_CLAIM_EXPIRED_TITLE import hedvig.resources.Res @@ -277,10 +277,19 @@ private fun HomeScreen( ) } if (showDraftExpiredDialog) { - ErrorDialog( + HedvigAlertDialog( title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), - message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), - onDismiss = { showDraftExpiredDialog = false }, + text = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), + confirmButtonLabel = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_START_NEW), + dismissButtonLabel = stringResource(Res.string.general_cancel_button), + onDismissRequest = { showDraftExpiredDialog = false }, + onConfirmClick = { + showDraftExpiredDialog = false + // Starting a new claim from the expired notice is the member's confirmation to drop the + // stale draft, so the expired draft is deleted without a second confirmation dialog. + draftClaim?.let { deleteDraftClaim(it.id) } + startClaimBottomSheetState.show(Unit) + }, ) } val draftIdToDelete = draftIdPendingDeleteConfirmation From ad3525fa1f4024338d340a1a380a3dcc5906a876 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Mon, 6 Jul 2026 13:02:14 +0200 Subject: [PATCH 28/28] Download strings --- app/core/core-resources/src/androidMain/res/values/strings.xml | 2 +- .../src/commonMain/composeResources/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/core-resources/src/androidMain/res/values/strings.xml b/app/core/core-resources/src/androidMain/res/values/strings.xml index 272df7a2a4..d0adb2eced 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -725,7 +725,7 @@ Start new claim You have a draft claim Please make a new claim - Please make a new claim + Your draft has expired Continue where you stopped Your claim is automatically saved for 7 days. Yes, leave diff --git a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml index 08b7376726..3849cceaa4 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -724,7 +724,7 @@ Start new claim You have a draft claim Please make a new claim - Please make a new claim + Your draft has expired Continue where you stopped Your claim is automatically saved for 7 days. Yes, leave