From 4093616f7e3a212498c2a8999021a18d51c87f1c Mon Sep 17 00:00:00 2001 From: mariiapanasetskaia Date: Mon, 29 Jun 2026 11:39:11 +0200 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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 From 96b1dfd32277f3a4bf39196c81e011dacdcbd359 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Mon, 6 Jul 2026 17:32:24 +0200 Subject: [PATCH 29/33] Align draft claim UI with Ready-for-dev design - Expired notice: single Close that removes the draft (matches Figma), replacing the two-button Start-new-claim/Cancel flow - Draft card: reuse the existing Submitted progress segment instead of a custom Started label - Draft card: red destructive label on Delete via a new HedvigSecondaryRedTextButton - Draft dialog: drop the Cancel button (Continue draft / Start new claim only) --- .../android/design/system/hedvig/Button.kt | 25 +++++++++++++++++++ .../design/system/hedvig/DraftClaimDialog.kt | 9 ------- .../feature/home/home/ui/HomeDestination.kt | 16 +++++------- .../android/ui/claimstatus/DraftClaimCard.kt | 8 +++--- .../claimstatus/internal/ClaimProgressRow.kt | 6 ----- .../claimstatus/model/ClaimProgressSegment.kt | 1 - 6 files changed, 34 insertions(+), 31 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt index d963ed99e7..a188b0b647 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Button.kt @@ -195,6 +195,31 @@ fun HedvigRedTextButton( } } +/** + * A secondary-fill button with a destructive (red) label, e.g. a "Delete" action that sits next to + * a primary action. Reuses the secondary style's red content token rather than a bespoke color. + */ +@Composable +fun HedvigSecondaryRedTextButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + buttonSize: ButtonSize = ButtonSize.Medium, + interactionSource: MutableInteractionSource? = null, +) { + HedvigButton( + onClick = onClick, + enabled = enabled, + modifier = modifier, + buttonStyle = ButtonDefaults.ButtonStyle.Secondary, + buttonSize = buttonSize, + interactionSource = interactionSource, + ) { + HedvigText(text, color = ButtonDefaults.ButtonStyle.Secondary.style.buttonColors.redTextColor) + } +} + @Composable fun HedvigButtonGhostWithBorder( text: String, 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 index 0642f4f140..bc7b2910b6 100644 --- 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 @@ -13,7 +13,6 @@ 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 @@ -56,14 +55,6 @@ fun DraftClaimDialog( 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(), - ) } } } 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 8d42febe2d..74d11b1379 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,6 +77,7 @@ 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 @@ -135,7 +136,6 @@ 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,18 +277,14 @@ private fun HomeScreen( ) } if (showDraftExpiredDialog) { - HedvigAlertDialog( + ErrorDialog( title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), - 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 = { + message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), + // The draft is expired, so acknowledging the notice (Close button, scrim, or back) removes + // it. Matches the Ready-for-dev design: single Close, closing removes the draft claim card. + onDismiss = { 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) }, ) } 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 index a8bd753027..3a3579e3f4 100644 --- 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 @@ -12,11 +12,11 @@ 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.HedvigSecondaryRedTextButton import com.hedvig.android.design.system.hedvig.HedvigText import com.hedvig.android.design.system.hedvig.HedvigTheme import com.hedvig.android.design.system.hedvig.HighlightLabel @@ -73,18 +73,16 @@ fun DraftClaimCard( Spacer(Modifier.height(18.dp)) ClaimProgressRow( claimProgressItemsUiState = listOf( - ClaimProgressSegment(SegmentText.Started, INACTIVE), + ClaimProgressSegment(SegmentText.Submitted, INACTIVE), ClaimProgressSegment(SegmentText.BeingHandled, INACTIVE), ClaimProgressSegment(SegmentText.Closed, INACTIVE), ), ) Spacer(Modifier.height(16.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - HedvigButton( + HedvigSecondaryRedTextButton( text = stringResource(Res.string.RESUME_CLAIM_DELETE_BUTTON), onClick = onDeleteClick, - enabled = true, - buttonStyle = Secondary, buttonSize = Medium, modifier = Modifier.weight(1f), ) 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 5356d34c97..36b5db2119 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,7 +25,6 @@ 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 @@ -80,13 +79,8 @@ 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/ClaimProgressSegment.kt b/app/ui/claim-status/src/main/kotlin/com/hedvig/android/ui/claimstatus/model/ClaimProgressSegment.kt index ca879b287a..71dc5400f0 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,7 +13,6 @@ data class ClaimProgressSegment( Submitted, BeingHandled, Closed, - Started, } enum class SegmentType { From bbd12bf999babcafcf0b61e4e62a36cca97bef28 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Tue, 7 Jul 2026 13:16:27 +0200 Subject: [PATCH 30/33] Lay out the draft dialog buttons horizontally with a red-text confirm Switch DraftClaimDialog to the design-system two-button dialog (like the Delete-draft alert) so the buttons sit side by side when they fit and stack otherwise. Add an opt-in confirmButtonRedText flag to DialogStyle.Buttons so the destructive 'Start new claim' confirm renders as a secondary fill with a red label, matching the Ready-for-dev design, wired through both the horizontal and vertical button layouts. Also relocate the expired-dialog comment out of the call arguments so ktlint keeps it. --- .../android/design/system/hedvig/Dialog.kt | 54 +++++++++++++------ .../design/system/hedvig/DraftClaimDialog.kt | 37 +++++-------- .../feature/home/home/ui/HomeDestination.kt | 4 +- 3 files changed, 54 insertions(+), 41 deletions(-) diff --git a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt index 635f35918e..080797e28b 100644 --- a/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt +++ b/app/design-system/design-system-hedvig/src/commonMain/kotlin/com/hedvig/android/design/system/hedvig/Dialog.kt @@ -291,6 +291,9 @@ object DialogDefaults { val onConfirmButtonClick: () -> Unit, val confirmButtonText: String, val buttonSize: ButtonSize = defaultButtonSize, + // When true the confirm button renders with a destructive (red) label on a secondary fill, + // instead of the default primary style. For dialogs where the confirm action is destructive. + val confirmButtonRedText: Boolean = false, ) : DialogStyle() /** @@ -368,6 +371,7 @@ private fun HedvigDialogContent( dismissButtonText = style.dismissButtonText, onConfirmButtonClick = style.onConfirmButtonClick, confirmButtonText = style.confirmButtonText, + confirmButtonRedText = style.confirmButtonRedText, ) } @@ -377,6 +381,7 @@ private fun HedvigDialogContent( dismissButtonText = style.dismissButtonText, onConfirmButtonClick = style.onConfirmButtonClick, confirmButtonText = style.confirmButtonText, + confirmButtonRedText = style.confirmButtonRedText, ) } } @@ -465,6 +470,7 @@ private fun SmallHorizontalPreferringButtons( dismissButtonText: String, onConfirmButtonClick: () -> Unit, confirmButtonText: String, + confirmButtonRedText: Boolean = false, ) { val textMeasurer = rememberTextMeasurer(cacheSize = 2) val textStyle = LocalTextStyle.current @@ -477,13 +483,21 @@ private fun SmallHorizontalPreferringButtons( buttonStyle = ButtonDefaults.ButtonStyle.Secondary, buttonSize = ButtonDefaults.ButtonSize.Medium, ) - HedvigButton( - onClick = onConfirmButtonClick, - text = confirmButtonText, - enabled = true, - buttonStyle = ButtonDefaults.ButtonStyle.Primary, - buttonSize = ButtonDefaults.ButtonSize.Medium, - ) + if (confirmButtonRedText) { + HedvigSecondaryRedTextButton( + onClick = onConfirmButtonClick, + text = confirmButtonText, + buttonSize = ButtonDefaults.ButtonSize.Medium, + ) + } else { + HedvigButton( + onClick = onConfirmButtonClick, + text = confirmButtonText, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Primary, + buttonSize = ButtonDefaults.ButtonSize.Medium, + ) + } }, ) { measurables, constraints -> val spaceBetween = 8.dp @@ -531,16 +545,26 @@ private fun BigVerticalButtons( dismissButtonText: String, onConfirmButtonClick: () -> Unit, confirmButtonText: String, + confirmButtonRedText: Boolean = false, ) { Column { - HedvigButton( - modifier = Modifier.fillMaxWidth(), - onClick = onConfirmButtonClick, - text = confirmButtonText, - enabled = true, - buttonStyle = ButtonDefaults.ButtonStyle.Primary, - buttonSize = ButtonDefaults.ButtonSize.Large, - ) + if (confirmButtonRedText) { + HedvigSecondaryRedTextButton( + modifier = Modifier.fillMaxWidth(), + onClick = onConfirmButtonClick, + text = confirmButtonText, + buttonSize = ButtonDefaults.ButtonSize.Large, + ) + } else { + HedvigButton( + modifier = Modifier.fillMaxWidth(), + onClick = onConfirmButtonClick, + text = confirmButtonText, + enabled = true, + buttonStyle = ButtonDefaults.ButtonStyle.Primary, + buttonSize = ButtonDefaults.ButtonSize.Large, + ) + } Spacer(Modifier.height(8.dp)) HedvigButton( modifier = Modifier.fillMaxWidth(), 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 index bc7b2910b6..2e260ee7ee 100644 --- 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 @@ -1,13 +1,11 @@ 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle.Buttons 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 @@ -22,38 +20,29 @@ fun DraftClaimDialog( onStartNewClaim: () -> Unit, modifier: Modifier = Modifier, ) { + // Two-button dialog: left "Continue draft" (safe), right "Start new claim" (destructive, red). + // SMALL keeps them side by side when they fit and stacks them otherwise. Dismissing via scrim or + // back does nothing beyond closing; both actions are wired to the explicit buttons. HedvigDialog( onDismissRequest = onDismissRequest, modifier = modifier, + style = Buttons( + onDismissRequest = onContinueDraft, + dismissButtonText = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_CONTINUE), + onConfirmButtonClick = onStartNewClaim, + confirmButtonText = stringResource(Res.string.RESUME_CLAIM_DRAFT_ALERT_START_NEW), + confirmButtonRedText = true, + ), ) { - Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { 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(), ) } } 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 74d11b1379..5d9603fe15 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 @@ -277,11 +277,11 @@ private fun HomeScreen( ) } if (showDraftExpiredDialog) { + // The draft is expired, so acknowledging the notice (Close button, scrim, or back) removes it. + // Matches the Ready-for-dev design: single Close, closing removes the draft claim card. ErrorDialog( title = stringResource(Res.string.RESUME_CLAIM_EXPIRED_TITLE), message = stringResource(Res.string.RESUME_CLAIM_EXPIRED_BODY), - // The draft is expired, so acknowledging the notice (Close button, scrim, or back) removes - // it. Matches the Ready-for-dev design: single Close, closing removes the draft claim card. onDismiss = { showDraftExpiredDialog = false draftClaim?.let { deleteDraftClaim(it.id) } From c7fb5305601973c12b209119d28357af79eb3462 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 9 Jul 2026 15:38:06 +0200 Subject: [PATCH 31/33] Download strings --- .../src/androidMain/res/values-sv-rSE/strings.xml | 10 +++++++++- .../src/androidMain/res/values/strings.xml | 8 ++++++++ .../composeResources/values-sv-rSE/strings.xml | 10 +++++++++- .../src/commonMain/composeResources/values/strings.xml | 8 ++++++++ 4 files changed, 34 insertions(+), 2 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 14e7373a3f..6bda39074f 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 @@ -26,6 +26,10 @@ Lägg till skydd Populär Aktiv + Mer skydd för bilen + Drulle: kupé, nycklar, feltankning + Hyrbil när din egen bil repareras + Självriskreducering vid kollision Bil Plus Fortsätt Den här tilläggsförsäkringen är aktiv och gäller ihop med din försäkring.\n\nVill du ta bort den behöver du göra det från försäkringen den är kopplad till. @@ -63,6 +67,10 @@ Avsluta %1$s Om du avslutar detta tillägg ingår inte det extra skyddet längre i din hemförsäkring. Avsluta %1$s + Extra trygghet när du reser + Res upp till 60 dagar i sträck + Försenat flyg och bagage + Gäller för alla medförsäkrade OK Lyssna Skicka @@ -730,7 +738,7 @@ Din anmälan sparas automatiskt i 7 dagar. Ja, lämna Lämna anmälan? - Borjade %1$s + Började %1$s Spara och fortsätt Inga träffar på din sökning Sök 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 d0adb2eced..0e0b87908c 100644 --- a/app/core/core-resources/src/androidMain/res/values/strings.xml +++ b/app/core/core-resources/src/androidMain/res/values/strings.xml @@ -26,6 +26,10 @@ Add coverage Popular Active + More for your car insurance + All-risk: interior, keys, misfuelling + Compensation for rental car + Lower deductible for collision Car Plus Continue This addon is active and linked to your insurance.\n\nTo remove it, go to the insurance it’s linked to. @@ -63,6 +67,10 @@ Cancel %1$s By canceling this extended coverage, your home insurance will no longer include this extra protection. Cancel %1$s + Add extra safety when traveling + Travel up to 60 days in a row + Delayed bags and flights + Applies to all co-insured OK Listen Send 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 12f10933e9..9b00cccdb5 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 @@ -25,6 +25,10 @@ Lägg till skydd Populär Aktiv + Mer skydd för bilen + Drulle: kupé, nycklar, feltankning + Hyrbil när din egen bil repareras + Självriskreducering vid kollision Bil Plus Fortsätt Den här tilläggsförsäkringen är aktiv och gäller ihop med din försäkring.\n\nVill du ta bort den behöver du göra det från försäkringen den är kopplad till. @@ -62,6 +66,10 @@ Avsluta %1$s Om du avslutar detta tillägg ingår inte det extra skyddet längre i din hemförsäkring. Avsluta %1$s + Extra trygghet när du reser + Res upp till 60 dagar i sträck + Försenat flyg och bagage + Gäller för alla medförsäkrade OK Lyssna Skicka @@ -729,7 +737,7 @@ Din anmälan sparas automatiskt i 7 dagar. Ja, lämna Lämna anmälan? - Borjade %1$s + Började %1$s Spara och fortsätt Inga träffar på din sökning Sök 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 3849cceaa4..eabd4337b2 100644 --- a/app/core/core-resources/src/commonMain/composeResources/values/strings.xml +++ b/app/core/core-resources/src/commonMain/composeResources/values/strings.xml @@ -25,6 +25,10 @@ Add coverage Popular Active + More for your car insurance + All-risk: interior, keys, misfuelling + Compensation for rental car + Lower deductible for collision Car Plus Continue This addon is active and linked to your insurance.\n\nTo remove it, go to the insurance it’s linked to. @@ -62,6 +66,10 @@ Cancel %1$s By canceling this extended coverage, your home insurance will no longer include this extra protection. Cancel %1$s + Add extra safety when traveling + Travel up to 60 days in a row + Delayed bags and flights + Applies to all co-insured OK Listen Send From 6c05117dcc9cdf746242aa1678a8ac559be21fc0 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Thu, 9 Jul 2026 15:52:01 +0200 Subject: [PATCH 32/33] Fix feature-home tests and formatting after develop merge The develop merge added two new tests (a cross-sell addon presenter test and a recommended-addon mapping test) that predate the resume-claim parameters, so their HomePresenter/HomeData/HomeQuery construction sites needed the deleteClaimIntentDraftUseCase, draftClaim, and resumeClaimEnabled arguments. Also reindent the flatMapLatest combine lambda in GetHomeDataUseCase. --- .../feature/home/home/data/GetHomeDataUseCase.kt | 12 ++++++------ .../feature/home/home/data/GetHomeUseCaseTest.kt | 2 +- .../feature/home/home/ui/HomePresenterTest.kt | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) 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 8ebabf6ed8..aa988c81e7 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 @@ -90,12 +90,12 @@ internal class GetHomeDataUseCaseImpl( featureManager.isFeatureEnabled(Feature.ENABLE_NEW_CONVERSATION_FROM_INBOX), hasAnyActiveConversationUseCase.invoke(alwaysHitTheNetwork = true), ) { - homeQueryDataResult, - unreadMessageCountResult, - memberReminders, - travelBannerInfo, - inboxAlwaysAvailable, - anyActiveConversations, + homeQueryDataResult, + unreadMessageCountResult, + memberReminders, + travelBannerInfo, + inboxAlwaysAvailable, + anyActiveConversations, -> either { val homeQueryData: HomeQuery.Data = homeQueryDataResult.bind() 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 b772637c36..5b9dd12977 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 @@ -691,7 +691,7 @@ internal class GetHomeUseCaseTest { val getHomeDataUseCase = testUseCaseWithoutReminders() apolloClient.registerTestResponse( - HomeQuery(true), + HomeQuery(true, true), HomeQuery.Data(OctopusFakeResolver) { currentMember = buildMember { crossSellV2 = buildCrossSellV2 { 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 7e33cb1595..fb65a87312 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 @@ -212,6 +212,7 @@ internal class HomePresenterTest { FakeCrossSellHomeNotificationService(), ApplicationScope(backgroundScope), false, + TestDeleteClaimIntentDraftUseCase(), ) val addonOnlyCrossSells = CrossSellSheetData(null, listOf(), testAddon) @@ -230,6 +231,7 @@ internal class HomePresenterTest { firstVetSections = listOf(), showHelpCenter = false, travelBannerInfo = null, + draftClaim = null, ).right(), ) assertThat(awaitItem()) From d295400bb16f9143a4392c26eaf8b2ded09de789 Mon Sep 17 00:00:00 2001 From: stylianosgakis Date: Fri, 10 Jul 2026 14:02:06 +0200 Subject: [PATCH 33/33] Run lint --- .../lint-baseline-data-claim-intent.xml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 hedvig-lint/lint-baseline/lint-baseline-data-claim-intent.xml diff --git a/hedvig-lint/lint-baseline/lint-baseline-data-claim-intent.xml b/hedvig-lint/lint-baseline/lint-baseline-data-claim-intent.xml new file mode 100644 index 0000000000..f23053f17a --- /dev/null +++ b/hedvig-lint/lint-baseline/lint-baseline-data-claim-intent.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + +