diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt index 4ea5a40d9f..8bc7ca36b7 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.raise.either import com.apollographql.apollo.ApolloClient import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import octopus.HousePriceIntentConfirmMutation @@ -23,6 +24,7 @@ internal interface SubmitHouseFormAndGetOffersUseCase { yearOfConstruction: Int, numberOfBathrooms: Int, isSubleted: Boolean, + extraBuildings: List, ): Either } @@ -41,6 +43,7 @@ internal class SubmitHouseFormAndGetOffersUseCaseImpl( yearOfConstruction: Int, numberOfBathrooms: Int, isSubleted: Boolean, + extraBuildings: List, ): Either { return either { val formData = buildMap { @@ -54,7 +57,16 @@ internal class SubmitHouseFormAndGetOffersUseCaseImpl( put("yearOfConstruction", yearOfConstruction) put("numberOfBathrooms", numberOfBathrooms) put("isSubleted", isSubleted) - put("extraBuildings", emptyList>()) + put( + "extraBuildings", + extraBuildings.map { building -> + mapOf( + "type" to building.type, + "area" to building.area, + "hasWaterConnected" to building.hasWaterConnected, + ) + }, + ) } val updateResult = apolloClient diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt index b025d56fd1..904f548971 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt @@ -6,6 +6,7 @@ import com.apollographql.apollo.ApolloClient import com.hedvig.android.apollo.safeExecute import com.hedvig.android.core.common.ErrorMessage import com.hedvig.android.core.uidata.UiMoney +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo import com.hedvig.android.logger.LogPriority import com.hedvig.android.logger.logcat import octopus.HousePriceIntentConfirmMutation @@ -25,6 +26,7 @@ internal interface SubmitVacationHomeFormAndGetOffersUseCase { hasWaterConnected: Boolean, numberOfBathrooms: Int, isSubleted: Boolean, + extraBuildings: List, ): Either } @@ -43,6 +45,7 @@ internal class SubmitVacationHomeFormAndGetOffersUseCaseImpl( hasWaterConnected: Boolean, numberOfBathrooms: Int, isSubleted: Boolean, + extraBuildings: List, ): Either { return either { val formData = buildMap { @@ -56,7 +59,16 @@ internal class SubmitVacationHomeFormAndGetOffersUseCaseImpl( put("hasWaterConnected", hasWaterConnected) put("numberOfBathrooms", numberOfBathrooms) put("isSubleted", isSubleted) - put("extraBuildings", emptyList>()) + put( + "extraBuildings", + extraBuildings.map { building -> + mapOf( + "type" to building.type, + "area" to building.area, + "hasWaterConnected" to building.hasWaterConnected, + ) + }, + ) } val updateResult = apolloClient diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt new file mode 100644 index 0000000000..2f233c1bd2 --- /dev/null +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt @@ -0,0 +1,275 @@ +package com.hedvig.android.feature.purchase.house.ui.extrabuildings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.PrimaryAlt +import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle.NoButtons +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigDialog +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTextButton +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.ErrorState.Error.WithoutMessage +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.ErrorState.NoError +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.TextFieldSize +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HedvigToggle +import com.hedvig.android.design.system.hedvig.HorizontalDivider +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.IconButton +import com.hedvig.android.design.system.hedvig.RadioGroup +import com.hedvig.android.design.system.hedvig.RadioGroupStyle +import com.hedvig.android.design.system.hedvig.RadioOption +import com.hedvig.android.design.system.hedvig.RadioOptionId +import com.hedvig.android.design.system.hedvig.ToggleDefaults.ToggleDefaultStyleSize +import com.hedvig.android.design.system.hedvig.ToggleDefaults.ToggleStyle +import com.hedvig.android.design.system.hedvig.icon.Close +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons + +internal data class ExtraBuildingInfo( + val area: Int, + val type: String, + val displayName: String, + val hasWaterConnected: Boolean, +) + +internal data class MoveExtraBuildingType( + val type: String, + val displayName: String, +) + +// TODO: Replace these English strings with stringResource(...) lookups when Lokalise keys +// exist for each building type's display name. Today the move flow gets these from the +// backend (extraBuildingTypesV2.displayName), but PriceIntent does not expose that field. +internal val allExtraBuildingTypes: List = listOf( + MoveExtraBuildingType("GARAGE", "Garage"), + MoveExtraBuildingType("CARPORT", "Carport"), + MoveExtraBuildingType("SHED", "Shed"), + MoveExtraBuildingType("STOREHOUSE", "Storehouse"), + MoveExtraBuildingType("FRIGGEBOD", "Friggebod"), + MoveExtraBuildingType("ATTEFALL", "Attefallshus"), + MoveExtraBuildingType("OUTHOUSE", "Outhouse"), + MoveExtraBuildingType("GUESTHOUSE", "Guesthouse"), + MoveExtraBuildingType("GAZEBO", "Gazebo"), + MoveExtraBuildingType("GREENHOUSE", "Greenhouse"), + MoveExtraBuildingType("SAUNA", "Sauna"), + MoveExtraBuildingType("BARN", "Barn"), + MoveExtraBuildingType("BOATHOUSE", "Boathouse"), + MoveExtraBuildingType("OTHER", "Other"), +) + +@Composable +internal fun ExtraBuildingsSection( + extraBuildings: List, + allowedExtraBuildings: List, + onAddBuilding: (ExtraBuildingInfo) -> Unit, + onRemoveBuilding: (ExtraBuildingInfo) -> Unit, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + var dialogOpen by rememberSaveable { mutableStateOf(false) } + if (dialogOpen) { + HedvigDialog( + contentPadding = PaddingValues(0.dp), + dialogProperties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = { dialogOpen = false }, + style = NoButtons, + ) { + AddExtraBuildingDialogContent( + allowedExtraBuildings = allowedExtraBuildings, + onSaveBuilding = { building -> + onAddBuilding(building) + dialogOpen = false + }, + dismissDialog = { dialogOpen = false }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + HedvigCard(modifier.fillMaxWidth()) { + Column( + Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 16.dp), + ) { + HedvigText( + // TODO: Add "Extra buildings" / "Extra byggnader" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_LABEL). + text = "Extra buildings", + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + if (extraBuildings.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(vertical = 12.dp), + ) { + for ((index, extraBuilding) in extraBuildings.withIndex()) { + if (index != 0) { + HorizontalDivider() + } + key(extraBuilding) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + HedvigText(extraBuilding.displayName) + HedvigText( + text = buildString { + append(extraBuilding.area) + // TODO: Add area suffix " m²" to Lokalise (or reuse CHANGE_ADDRESS_SIZE_SUFFIX). + append(" m²") + if (extraBuilding.hasWaterConnected) { + append(" ∙ ") + // TODO: Add "Water connected" / "Vattenanslutet" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_WATER_LABEL). + append("Water connected") + } + }, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + IconButton( + onClick = { onRemoveBuilding(extraBuilding) }, + enabled = enabled, + ) { + // TODO: Add "Remove" / "Ta bort" content description to Lokalise (or reuse GENERAL_REMOVE). + Icon(HedvigIcons.Close, "Remove", Modifier.size(16.dp)) + } + } + } + } + } + } else { + Spacer(Modifier.height(8.dp)) + } + HedvigButton( + // TODO: Add "Add extra building" / "Lägg till extra byggnad" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_BOTTOM_SHEET_TITLE). + text = "Add extra building", + onClick = { dialogOpen = true }, + enabled = enabled, + buttonStyle = PrimaryAlt, + buttonSize = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +private fun AddExtraBuildingDialogContent( + allowedExtraBuildings: List, + onSaveBuilding: (ExtraBuildingInfo) -> Unit, + dismissDialog: () -> Unit, + modifier: Modifier = Modifier, +) { + var chosenBuilding: MoveExtraBuildingType? by remember { mutableStateOf(null) } + var size: Int? by remember { mutableStateOf(null) } + var isConnectedToWater: Boolean by remember { mutableStateOf(false) } + var isSizeMissing by remember { mutableStateOf(false) } + Column(modifier) { + Spacer(Modifier.height(16.dp)) + HedvigText( + // TODO: Add "Add extra building" / "Lägg till extra byggnad" to Lokalise. + text = "Add extra building", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) + .semantics { heading() }, + ) + Spacer(Modifier.height(8.dp)) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Spacer(Modifier.height(8.dp)) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + HedvigCard { + RadioGroup( + options = allowedExtraBuildings.map { buildingType -> + RadioOption( + RadioOptionId(buildingType.type), + buildingType.displayName, + ) + }, + selectedOption = chosenBuilding?.type?.let { RadioOptionId(it) }, + onRadioOptionSelected = { selected -> + chosenBuilding = allowedExtraBuildings.firstOrNull { it.type == selected.id } + }, + // TODO: Add "Type of building" / "Typ av byggnad" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDING_CONTAINER_TITLE). + style = RadioGroupStyle.Labeled.VerticalWithDivider("Type of building"), + ) + } + HedvigTextField( + text = size?.toString() ?: "", + onValueChange = { + isSizeMissing = false + size = it.toIntOrNull() + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + // TODO: Add "Size (m²)" / "Yta (m²)" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDING_SIZE_LABEL). + labelText = "Size (m²)", + textFieldSize = TextFieldSize.Medium, + errorState = if (isSizeMissing) WithoutMessage else NoError, + ) + HedvigToggle( + // TODO: Add "Water connected" / "Vattenanslutet" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_WATER_INPUT_LABEL). + labelText = "Water connected", + turnedOn = isConnectedToWater, + onClick = { isConnectedToWater = it }, + enabled = true, + toggleStyle = ToggleStyle.Default(ToggleDefaultStyleSize.Medium), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + // TODO: Add "Save" / "Spara" to Lokalise (or reuse general_save_button). + text = "Save", + onClick = { + if (size == null) { + isSizeMissing = true + } + val area = size ?: return@HedvigButton + val type = chosenBuilding?.type ?: return@HedvigButton + val displayName = chosenBuilding?.displayName ?: return@HedvigButton + onSaveBuilding(ExtraBuildingInfo(area, type, displayName, isConnectedToWater)) + }, + enabled = chosenBuilding != null, + buttonSize = ButtonSize.Large, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigTextButton( + // TODO: Add "Cancel" / "Avbryt" to Lokalise (or reuse general_cancel_button). + text = "Cancel", + onClick = dismissDialog, + enabled = true, + buttonSize = ButtonSize.Large, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } +} diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt index a3024e40f1..bb0014d70c 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt @@ -38,6 +38,9 @@ import com.hedvig.android.design.system.hedvig.StepperDefaults.StepperSize.Mediu import com.hedvig.android.design.system.hedvig.StepperDefaults.StepperStyle.Labeled import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.feature.purchase.house.data.HouseOffers +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingsSection +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.allExtraBuildingTypes @Composable internal fun HouseFormDestination( @@ -97,6 +100,7 @@ private fun HouseFormBody(uiState: HouseFormState, onEvent: (HouseFormEvent) -> yearOfConstruction = yearOfConstruction, numberOfBathrooms = numberOfBathrooms, isSubleted = uiState.isSubleted, + extraBuildings = uiState.extraBuildings, errors = uiState, isSubmitting = uiState.isSubmitting, onStreetChanged = { street = it }, @@ -113,6 +117,8 @@ private fun HouseFormBody(uiState: HouseFormState, onEvent: (HouseFormEvent) -> }, onNumberOfBathroomsChanged = { numberOfBathrooms = it }, onIsSubletedSelected = { onEvent(HouseFormEvent.UpdateIsSubleted(it)) }, + onAddExtraBuilding = { onEvent(HouseFormEvent.AddExtraBuilding(it)) }, + onRemoveExtraBuilding = { onEvent(HouseFormEvent.RemoveExtraBuilding(it)) }, onSubmit = { onEvent( HouseFormEvent.SubmitForm( @@ -139,6 +145,7 @@ private fun HouseFormContent( yearOfConstruction: String, numberOfBathrooms: Int, isSubleted: Boolean?, + extraBuildings: List, errors: HouseFormState, isSubmitting: Boolean, onStreetChanged: (String) -> Unit, @@ -149,6 +156,8 @@ private fun HouseFormContent( onYearOfConstructionChanged: (String) -> Unit, onNumberOfBathroomsChanged: (Int) -> Unit, onIsSubletedSelected: (Boolean) -> Unit, + onAddExtraBuilding: (ExtraBuildingInfo) -> Unit, + onRemoveExtraBuilding: (ExtraBuildingInfo) -> Unit, onSubmit: () -> Unit, ) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { @@ -253,6 +262,15 @@ private fun HouseFormContent( ) } Spacer(Modifier.height(16.dp)) + ExtraBuildingsSection( + extraBuildings = extraBuildings, + allowedExtraBuildings = allExtraBuildingTypes, + onAddBuilding = onAddExtraBuilding, + onRemoveBuilding = onRemoveExtraBuilding, + enabled = !isSubmitting, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) HedvigButton( // TODO: Add "Calculate price" / "Beräkna pris" to Lokalise text = "Calculate price", @@ -320,6 +338,7 @@ private fun PreviewHouseFormEmpty() { yearOfConstruction = "", numberOfBathrooms = 1, isSubleted = null, + extraBuildings = emptyList(), errors = HouseFormState(), isSubmitting = false, onStreetChanged = {}, @@ -330,6 +349,8 @@ private fun PreviewHouseFormEmpty() { onYearOfConstructionChanged = {}, onNumberOfBathroomsChanged = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } @@ -350,6 +371,7 @@ private fun PreviewHouseFormFilled() { yearOfConstruction = "1985", numberOfBathrooms = 2, isSubleted = false, + extraBuildings = emptyList(), errors = HouseFormState(), isSubmitting = false, onStreetChanged = {}, @@ -360,6 +382,8 @@ private fun PreviewHouseFormFilled() { onYearOfConstructionChanged = {}, onNumberOfBathroomsChanged = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } @@ -380,6 +404,7 @@ private fun PreviewHouseFormErrors() { yearOfConstruction = "1500", numberOfBathrooms = 1, isSubleted = null, + extraBuildings = emptyList(), errors = HouseFormState( streetError = "Enter an address", zipCodeError = "Enter a valid zip code (5 digits)", @@ -397,6 +422,8 @@ private fun PreviewHouseFormErrors() { onYearOfConstructionChanged = {}, onNumberOfBathroomsChanged = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt index 56f170e657..b0b2add670 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt @@ -11,6 +11,7 @@ import com.hedvig.android.feature.purchase.house.data.CreateHouseSessionAndPrice import com.hedvig.android.feature.purchase.house.data.HouseOffers import com.hedvig.android.feature.purchase.house.data.SessionAndIntent import com.hedvig.android.feature.purchase.house.data.SubmitHouseFormAndGetOffersUseCase +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -32,6 +33,10 @@ internal class HouseFormViewModel( internal sealed interface HouseFormEvent { data class UpdateIsSubleted(val value: Boolean) : HouseFormEvent + data class AddExtraBuilding(val building: ExtraBuildingInfo) : HouseFormEvent + + data class RemoveExtraBuilding(val building: ExtraBuildingInfo) : HouseFormEvent + data class SubmitForm( val street: String, val zipCode: String, @@ -51,6 +56,7 @@ internal sealed interface HouseFormEvent { internal data class HouseFormState( val isSubleted: Boolean? = null, + val extraBuildings: List = emptyList(), val streetError: String? = null, val zipCodeError: String? = null, val livingSpaceError: String? = null, @@ -88,6 +94,16 @@ private class HouseFormPresenter( currentState = currentState.copy(isSubleted = event.value, isSubletedError = null) } + is HouseFormEvent.AddExtraBuilding -> { + currentState = currentState.copy(extraBuildings = currentState.extraBuildings + event.building) + } + + is HouseFormEvent.RemoveExtraBuilding -> { + currentState = currentState.copy( + extraBuildings = currentState.extraBuildings.filterNot { it == event.building }, + ) + } + is HouseFormEvent.SubmitForm -> { val errors = validate(event, currentState) currentState = currentState.copy( @@ -157,6 +173,7 @@ private class HouseFormPresenter( yearOfConstruction = yearOfConstruction, numberOfBathrooms = submit.numberOfBathrooms, isSubleted = isSubleted, + extraBuildings = currentState.extraBuildings, ).fold( ifLeft = { error -> currentState = currentState.copy( diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt index 829799a03b..cd7e687a4b 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt @@ -38,6 +38,9 @@ import com.hedvig.android.design.system.hedvig.StepperDefaults.StepperSize.Mediu import com.hedvig.android.design.system.hedvig.StepperDefaults.StepperStyle.Labeled import com.hedvig.android.design.system.hedvig.Surface import com.hedvig.android.feature.purchase.house.data.HouseOffers +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingsSection +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.allExtraBuildingTypes @Composable internal fun VacationHomeFormDestination( @@ -95,6 +98,7 @@ private fun VacationHomeFormBody(uiState: VacationHomeFormState, onEvent: (Vacat multipleOwners = uiState.multipleOwners, hasWaterConnected = uiState.hasWaterConnected, isSubleted = uiState.isSubleted, + extraBuildings = uiState.extraBuildings, errors = uiState, isSubmitting = uiState.isSubmitting, onStreetChanged = { street = it }, @@ -109,6 +113,8 @@ private fun VacationHomeFormBody(uiState: VacationHomeFormState, onEvent: (Vacat onMultipleOwnersSelected = { onEvent(VacationHomeFormEvent.UpdateMultipleOwners(it)) }, onHasWaterConnectedSelected = { onEvent(VacationHomeFormEvent.UpdateHasWaterConnected(it)) }, onIsSubletedSelected = { onEvent(VacationHomeFormEvent.UpdateIsSubleted(it)) }, + onAddExtraBuilding = { onEvent(VacationHomeFormEvent.AddExtraBuilding(it)) }, + onRemoveExtraBuilding = { onEvent(VacationHomeFormEvent.RemoveExtraBuilding(it)) }, onSubmit = { onEvent( VacationHomeFormEvent.SubmitForm( @@ -133,6 +139,7 @@ private fun VacationHomeFormContent( multipleOwners: Boolean?, hasWaterConnected: Boolean?, isSubleted: Boolean?, + extraBuildings: List, errors: VacationHomeFormState, isSubmitting: Boolean, onStreetChanged: (String) -> Unit, @@ -143,6 +150,8 @@ private fun VacationHomeFormContent( onMultipleOwnersSelected: (Boolean) -> Unit, onHasWaterConnectedSelected: (Boolean) -> Unit, onIsSubletedSelected: (Boolean) -> Unit, + onAddExtraBuilding: (ExtraBuildingInfo) -> Unit, + onRemoveExtraBuilding: (ExtraBuildingInfo) -> Unit, onSubmit: () -> Unit, ) { Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)) { @@ -253,6 +262,15 @@ private fun VacationHomeFormContent( ) } Spacer(Modifier.height(16.dp)) + ExtraBuildingsSection( + extraBuildings = extraBuildings, + allowedExtraBuildings = allExtraBuildingTypes, + onAddBuilding = onAddExtraBuilding, + onRemoveBuilding = onRemoveExtraBuilding, + enabled = !isSubmitting, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) HedvigButton( // TODO: Add "Calculate price" / "Beräkna pris" to Lokalise text = "Calculate price", @@ -320,6 +338,7 @@ private fun PreviewVacationHomeFormEmpty() { multipleOwners = null, hasWaterConnected = null, isSubleted = null, + extraBuildings = emptyList(), errors = VacationHomeFormState(), isSubmitting = false, onStreetChanged = {}, @@ -330,6 +349,8 @@ private fun PreviewVacationHomeFormEmpty() { onMultipleOwnersSelected = {}, onHasWaterConnectedSelected = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } @@ -350,6 +371,7 @@ private fun PreviewVacationHomeFormFilled() { multipleOwners = false, hasWaterConnected = true, isSubleted = false, + extraBuildings = emptyList(), errors = VacationHomeFormState(), isSubmitting = false, onStreetChanged = {}, @@ -360,6 +382,8 @@ private fun PreviewVacationHomeFormFilled() { onMultipleOwnersSelected = {}, onHasWaterConnectedSelected = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } @@ -380,6 +404,7 @@ private fun PreviewVacationHomeFormErrors() { multipleOwners = null, hasWaterConnected = null, isSubleted = null, + extraBuildings = emptyList(), errors = VacationHomeFormState( streetError = "Enter an address", zipCodeError = "Enter a valid zip code (5 digits)", @@ -398,6 +423,8 @@ private fun PreviewVacationHomeFormErrors() { onMultipleOwnersSelected = {}, onHasWaterConnectedSelected = {}, onIsSubletedSelected = {}, + onAddExtraBuilding = {}, + onRemoveExtraBuilding = {}, onSubmit = {}, ) } diff --git a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt index 7e483937a7..1bb87197cb 100644 --- a/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt +++ b/app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt @@ -11,6 +11,7 @@ import com.hedvig.android.feature.purchase.house.data.CreateHouseSessionAndPrice import com.hedvig.android.feature.purchase.house.data.HouseOffers import com.hedvig.android.feature.purchase.house.data.SessionAndIntent import com.hedvig.android.feature.purchase.house.data.SubmitVacationHomeFormAndGetOffersUseCase +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo import com.hedvig.android.molecule.public.MoleculePresenter import com.hedvig.android.molecule.public.MoleculePresenterScope import com.hedvig.android.molecule.public.MoleculeViewModel @@ -36,6 +37,10 @@ internal sealed interface VacationHomeFormEvent { data class UpdateIsSubleted(val value: Boolean) : VacationHomeFormEvent + data class AddExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent + + data class RemoveExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent + data class SubmitForm( val street: String, val zipCode: String, @@ -55,6 +60,7 @@ internal data class VacationHomeFormState( val multipleOwners: Boolean? = null, val hasWaterConnected: Boolean? = null, val isSubleted: Boolean? = null, + val extraBuildings: List = emptyList(), val streetError: String? = null, val zipCodeError: String? = null, val multipleOwnersError: String? = null, @@ -103,6 +109,16 @@ private class VacationHomeFormPresenter( currentState = currentState.copy(isSubleted = event.value, isSubletedError = null) } + is VacationHomeFormEvent.AddExtraBuilding -> { + currentState = currentState.copy(extraBuildings = currentState.extraBuildings + event.building) + } + + is VacationHomeFormEvent.RemoveExtraBuilding -> { + currentState = currentState.copy( + extraBuildings = currentState.extraBuildings.filterNot { it == event.building }, + ) + } + is VacationHomeFormEvent.SubmitForm -> { val errors = validate(event, currentState) currentState = currentState.copy( @@ -174,6 +190,7 @@ private class VacationHomeFormPresenter( hasWaterConnected = hasWaterConnected, numberOfBathrooms = submit.numberOfBathrooms, isSubleted = isSubleted, + extraBuildings = currentState.extraBuildings, ).fold( ifLeft = { error -> currentState = currentState.copy( diff --git a/docs/superpowers/plans/2026-05-22-extra-buildings-in-house-purchase.md b/docs/superpowers/plans/2026-05-22-extra-buildings-in-house-purchase.md new file mode 100644 index 0000000000..9f349bc727 --- /dev/null +++ b/docs/superpowers/plans/2026-05-22-extra-buildings-in-house-purchase.md @@ -0,0 +1,644 @@ +# Extra Buildings in House Purchase 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:** Let the vacation-home (fritidshus) and villa (SE_HOUSE) in-app purchase forms collect a list of extra buildings (garage, shed, etc.) and submit them as part of the existing `priceIntentDataUpdate` mutation, so returned offers reflect the declared buildings. + +**Architecture:** Copy the section + dialog UI from the move flow's `AddHouseInformationDestination.kt` into a new file in `feature-purchase-house/.../ui/extrabuildings/`. Adapt to plain `List` + callbacks (no `ListInput` wrapper). Wire two new events (`AddExtraBuilding`, `RemoveExtraBuilding`) into both `VacationHomeFormViewModel` and `HouseFormViewModel`, render the section in both destinations, and serialize the list into the existing `PricingFormData` map inside both submit use cases. No backend changes, no incremental mutations, no edit affordance. + +**Tech Stack:** Kotlin, Jetpack Compose, Apollo GraphQL, Molecule (MVI), Koin DI, Arrow (Either), Hedvig design system. + +**Base branch:** This work must touch both the vacation-home form and the villa form. The villa files (`HouseFormViewModel`, `HouseFormDestination`, `SubmitHouseFormAndGetOffersUseCase`, branched `HousePurchaseNavGraph`) live only on `feat/in-app-house-purchase`. The implementation branch **must** be cut from `feat/in-app-house-purchase` (not `develop`, not `feat/in-app-vacation-home-purchase`). + +**Spec:** [docs/superpowers/specs/2026-05-22-extra-buildings-in-house-purchase-design.md](../specs/2026-05-22-extra-buildings-in-house-purchase-design.md) + +--- + +### Task 0: Verify base branch + +**Files:** none + +- [ ] **Step 1: Confirm branch is based off `feat/in-app-house-purchase`** + +Run: `git merge-base --is-ancestor feat/in-app-house-purchase HEAD && echo "OK: stacked on house" || echo "FAIL: not stacked on house"` +Expected: `OK: stacked on house`. + +If this fails, stop and rebranch from `feat/in-app-house-purchase` before continuing — the villa form files won't exist otherwise and Tasks 3 / 4 will be impossible. + +- [ ] **Step 2: Confirm both target form files exist** + +Run: +```bash +ls app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt +``` +Expected: all six paths exist with no `No such file or directory`. + +- [ ] **Step 3: Confirm the move flow's reference UI is present (we'll be copying from it)** + +Run: `ls app/feature/feature-movingflow/src/main/kotlin/com/hedvig/android/feature/movingflow/ui/addhouseinformation/AddHouseInformationDestination.kt` +Expected: file exists. + +--- + +### Task 1: Create the shared extra-buildings file + +**Files:** +- Create: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt` + +- [ ] **Step 1: Create the new file with data types, hardcoded type list, and copied composables** + +File: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt` + +```kotlin +package com.hedvig.android.feature.purchase.house.ui.extrabuildings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.heading +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonSize +import com.hedvig.android.design.system.hedvig.ButtonDefaults.ButtonStyle.PrimaryAlt +import com.hedvig.android.design.system.hedvig.DialogDefaults.DialogStyle.NoButtons +import com.hedvig.android.design.system.hedvig.HedvigButton +import com.hedvig.android.design.system.hedvig.HedvigCard +import com.hedvig.android.design.system.hedvig.HedvigDialog +import com.hedvig.android.design.system.hedvig.HedvigText +import com.hedvig.android.design.system.hedvig.HedvigTextButton +import com.hedvig.android.design.system.hedvig.HedvigTextField +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.ErrorState.Error.WithoutMessage +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.ErrorState.NoError +import com.hedvig.android.design.system.hedvig.HedvigTextFieldDefaults.TextFieldSize +import com.hedvig.android.design.system.hedvig.HedvigTheme +import com.hedvig.android.design.system.hedvig.HedvigToggle +import com.hedvig.android.design.system.hedvig.HorizontalDivider +import com.hedvig.android.design.system.hedvig.Icon +import com.hedvig.android.design.system.hedvig.IconButton +import com.hedvig.android.design.system.hedvig.RadioGroup +import com.hedvig.android.design.system.hedvig.RadioGroupStyle +import com.hedvig.android.design.system.hedvig.RadioOption +import com.hedvig.android.design.system.hedvig.RadioOptionId +import com.hedvig.android.design.system.hedvig.ToggleDefaults.ToggleDefaultStyleSize +import com.hedvig.android.design.system.hedvig.ToggleDefaults.ToggleStyle +import com.hedvig.android.design.system.hedvig.icon.Close +import com.hedvig.android.design.system.hedvig.icon.HedvigIcons + +internal data class ExtraBuildingInfo( + val area: Int, + val type: String, + val displayName: String, + val hasWaterConnected: Boolean, +) + +internal data class MoveExtraBuildingType( + val type: String, + val displayName: String, +) + +// TODO: Replace these English strings with stringResource(...) lookups when Lokalise keys +// exist for each building type's display name. Today the move flow gets these from the +// backend (extraBuildingTypesV2.displayName), but PriceIntent does not expose that field. +internal val allExtraBuildingTypes: List = listOf( + MoveExtraBuildingType("GARAGE", "Garage"), + MoveExtraBuildingType("CARPORT", "Carport"), + MoveExtraBuildingType("SHED", "Shed"), + MoveExtraBuildingType("STOREHOUSE", "Storehouse"), + MoveExtraBuildingType("FRIGGEBOD", "Friggebod"), + MoveExtraBuildingType("ATTEFALL", "Attefallshus"), + MoveExtraBuildingType("OUTHOUSE", "Outhouse"), + MoveExtraBuildingType("GUESTHOUSE", "Guesthouse"), + MoveExtraBuildingType("GAZEBO", "Gazebo"), + MoveExtraBuildingType("GREENHOUSE", "Greenhouse"), + MoveExtraBuildingType("SAUNA", "Sauna"), + MoveExtraBuildingType("BARN", "Barn"), + MoveExtraBuildingType("BOATHOUSE", "Boathouse"), + MoveExtraBuildingType("OTHER", "Other"), +) + +@Composable +internal fun ExtraBuildingsSection( + extraBuildings: List, + allowedExtraBuildings: List, + onAddBuilding: (ExtraBuildingInfo) -> Unit, + onRemoveBuilding: (ExtraBuildingInfo) -> Unit, + enabled: Boolean, + modifier: Modifier = Modifier, +) { + var dialogOpen by rememberSaveable { mutableStateOf(false) } + if (dialogOpen) { + HedvigDialog( + contentPadding = PaddingValues(0.dp), + dialogProperties = DialogProperties(usePlatformDefaultWidth = false), + onDismissRequest = { dialogOpen = false }, + style = NoButtons, + ) { + AddExtraBuildingDialogContent( + allowedExtraBuildings = allowedExtraBuildings, + onSaveBuilding = { building -> + onAddBuilding(building) + dialogOpen = false + }, + dismissDialog = { dialogOpen = false }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + HedvigCard(modifier.fillMaxWidth()) { + Column( + Modifier.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 16.dp), + ) { + HedvigText( + // TODO: Add "Extra buildings" / "Extra byggnader" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_LABEL). + text = "Extra buildings", + style = HedvigTheme.typography.label, + color = HedvigTheme.colorScheme.textSecondary, + ) + if (extraBuildings.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(vertical = 12.dp), + ) { + for ((index, extraBuilding) in extraBuildings.withIndex()) { + if (index != 0) { + HorizontalDivider() + } + key(extraBuilding) { + Row(verticalAlignment = Alignment.CenterVertically) { + Column(modifier = Modifier.weight(1f)) { + HedvigText(extraBuilding.displayName) + HedvigText( + text = buildString { + append(extraBuilding.area) + // TODO: Add area suffix " m²" to Lokalise (or reuse CHANGE_ADDRESS_SIZE_SUFFIX). + append(" m²") + if (extraBuilding.hasWaterConnected) { + append(" ∙ ") + // TODO: Add "Water connected" / "Vattenanslutet" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_WATER_LABEL). + append("Water connected") + } + }, + color = HedvigTheme.colorScheme.textSecondary, + style = HedvigTheme.typography.label, + ) + } + IconButton( + onClick = { onRemoveBuilding(extraBuilding) }, + enabled = enabled, + ) { + // TODO: Add "Remove" / "Ta bort" content description to Lokalise (or reuse GENERAL_REMOVE). + Icon(HedvigIcons.Close, "Remove", Modifier.size(16.dp)) + } + } + } + } + } + } else { + Spacer(Modifier.height(8.dp)) + } + HedvigButton( + // TODO: Add "Add extra building" / "Lägg till extra byggnad" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_BOTTOM_SHEET_TITLE). + text = "Add extra building", + onClick = { dialogOpen = true }, + enabled = enabled, + buttonStyle = PrimaryAlt, + buttonSize = ButtonSize.Medium, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} + +@Composable +private fun AddExtraBuildingDialogContent( + allowedExtraBuildings: List, + onSaveBuilding: (ExtraBuildingInfo) -> Unit, + dismissDialog: () -> Unit, + modifier: Modifier = Modifier, +) { + var chosenBuilding: MoveExtraBuildingType? by remember { mutableStateOf(null) } + var size: Int? by remember { mutableStateOf(null) } + var isConnectedToWater: Boolean by remember { mutableStateOf(false) } + var isSizeMissing by remember { mutableStateOf(false) } + Column(modifier) { + Spacer(Modifier.height(16.dp)) + HedvigText( + // TODO: Add "Add extra building" / "Lägg till extra byggnad" to Lokalise. + text = "Add extra building", + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) + .semantics { heading() }, + ) + Spacer(Modifier.height(8.dp)) + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Spacer(Modifier.height(8.dp)) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + HedvigCard { + RadioGroup( + options = allowedExtraBuildings.map { buildingType -> + RadioOption( + RadioOptionId(buildingType.type), + buildingType.displayName, + ) + }, + selectedOption = chosenBuilding?.type?.let { RadioOptionId(it) }, + onRadioOptionSelected = { selected -> + chosenBuilding = allowedExtraBuildings.firstOrNull { it.type == selected.id } + }, + // TODO: Add "Type of building" / "Typ av byggnad" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDING_CONTAINER_TITLE). + style = RadioGroupStyle.Labeled.VerticalWithDivider("Type of building"), + ) + } + HedvigTextField( + text = size?.toString() ?: "", + onValueChange = { + isSizeMissing = false + size = it.toIntOrNull() + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + // TODO: Add "Size (m²)" / "Yta (m²)" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDING_SIZE_LABEL). + labelText = "Size (m²)", + textFieldSize = TextFieldSize.Medium, + errorState = if (isSizeMissing) WithoutMessage else NoError, + ) + HedvigToggle( + // TODO: Add "Water connected" / "Vattenanslutet" to Lokalise (or reuse CHANGE_ADDRESS_EXTRA_BUILDINGS_WATER_INPUT_LABEL). + labelText = "Water connected", + turnedOn = isConnectedToWater, + onClick = { isConnectedToWater = it }, + enabled = true, + toggleStyle = ToggleStyle.Default(ToggleDefaultStyleSize.Medium), + ) + } + Spacer(Modifier.height(16.dp)) + HedvigButton( + // TODO: Add "Save" / "Spara" to Lokalise (or reuse general_save_button). + text = "Save", + onClick = { + if (size == null) { + isSizeMissing = true + } + val area = size ?: return@HedvigButton + val type = chosenBuilding?.type ?: return@HedvigButton + val displayName = chosenBuilding?.displayName ?: return@HedvigButton + onSaveBuilding(ExtraBuildingInfo(area, type, displayName, isConnectedToWater)) + }, + enabled = chosenBuilding != null, + buttonSize = ButtonSize.Large, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + HedvigTextButton( + // TODO: Add "Cancel" / "Avbryt" to Lokalise (or reuse general_cancel_button). + text = "Cancel", + onClick = dismissDialog, + enabled = true, + buttonSize = ButtonSize.Large, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(16.dp)) + } + } +} +``` + +- [ ] **Step 2: Format + compile-check the module** + +Run: `./gradlew :feature-purchase-house:ktlintFormat :feature-purchase-house:compileDebugKotlin --quiet` +Expected: completes without errors. Compilation should succeed because no other file references the new symbols yet. + +- [ ] **Step 3: Commit** + +```bash +git add app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt +git commit -m "feat: add ExtraBuildingsSection for house purchase forms" +``` + +--- + +### Task 2: Wire extra buildings into the vacation-home flow + +**Files:** +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt` +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt` +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt` + +- [ ] **Step 1: Add `extraBuildings` field to `VacationHomeFormState` and two new events** + +In `VacationHomeFormViewModel.kt`: + +1. At the top of the file, add the import: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +``` + +2. In the `VacationHomeFormEvent` sealed interface, add two new event types alongside the existing ones: + +```kotlin +data class AddExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent + +data class RemoveExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent +``` + +3. In `VacationHomeFormState`, add this field (default empty list): + +```kotlin +val extraBuildings: List = emptyList(), +``` + +- [ ] **Step 2: Handle the two new events in the presenter** + +In the same file, inside `VacationHomeFormPresenter.present`'s `CollectEvents { event -> when (event) { ... } }` block, add two new branches: + +```kotlin +is VacationHomeFormEvent.AddExtraBuilding -> { + currentState = currentState.copy(extraBuildings = currentState.extraBuildings + event.building) +} + +is VacationHomeFormEvent.RemoveExtraBuilding -> { + currentState = currentState.copy( + extraBuildings = currentState.extraBuildings.filterNot { it == event.building }, + ) +} +``` + +- [ ] **Step 3: Pass extra buildings into the submit use case** + +In the same file's `LaunchedEffect(submitIteration) { ... }` block, find the call to `submitVacationHomeFormAndGetOffersUseCase.invoke(...)`. Add one final argument before the closing parenthesis: + +```kotlin +extraBuildings = currentState.extraBuildings, +``` + +- [ ] **Step 4: Render `ExtraBuildingsSection` in the destination** + +In `VacationHomeFormDestination.kt`: + +1. Add the imports: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingsSection +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.allExtraBuildingTypes +``` + +2. In the destination's content composable (the place where the existing form fields are laid out), after the existing `isSubleted` field and before the submit `HedvigButton`, insert: + +```kotlin +Spacer(Modifier.height(16.dp)) +ExtraBuildingsSection( + extraBuildings = uiState.extraBuildings, + allowedExtraBuildings = allExtraBuildingTypes, + onAddBuilding = { onEvent(VacationHomeFormEvent.AddExtraBuilding(it)) }, + onRemoveBuilding = { onEvent(VacationHomeFormEvent.RemoveExtraBuilding(it)) }, + enabled = !uiState.isSubmitting, + modifier = Modifier.fillMaxWidth(), +) +``` + +If the surrounding scope already imports `Spacer`, `Modifier`, `fillMaxWidth`, and `height`, do not duplicate those imports. Otherwise add them. + +- [ ] **Step 5: Add the new parameter to `SubmitVacationHomeFormAndGetOffersUseCase`** + +In `SubmitVacationHomeFormAndGetOffersUseCase.kt`: + +1. Add the import: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +``` + +2. Add a parameter to `invoke(...)`. The current signature lists each form field; add `extraBuildings: List` at the end of the parameter list. + +3. Inside the function body, replace the placeholder line at [SubmitVacationHomeFormAndGetOffersUseCase.kt:59](app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt:59): + +```kotlin +put("extraBuildings", emptyList>()) +``` + +with: + +```kotlin +put( + "extraBuildings", + extraBuildings.map { building -> + mapOf( + "type" to building.type, + "area" to building.area, + "hasWaterConnected" to building.hasWaterConnected, + ) + }, +) +``` + +- [ ] **Step 6: Format + compile-check** + +Run: `./gradlew :feature-purchase-house:ktlintFormat :feature-purchase-house:compileDebugKotlin --quiet` +Expected: completes without errors. + +- [ ] **Step 7: Commit** + +```bash +git add app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt +git commit -m "feat: wire extra buildings into vacation home purchase flow" +``` + +--- + +### Task 3: Wire extra buildings into the villa (SE_HOUSE) flow + +**Files:** +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt` +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt` +- Modify: `app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt` + +The villa form mirrors the vacation home form structure (the commit `de584b9146 refactor: align vacation home form with pet form pattern` was applied across both). Apply identical changes; do **not** assume any helper from Task 2 is reusable across the two flows beyond the file from Task 1. + +- [ ] **Step 1: Add `extraBuildings` field to `HouseFormState` and two new events** + +In `HouseFormViewModel.kt`: + +1. Add the import: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +``` + +2. In the `HouseFormEvent` sealed interface, add: + +```kotlin +data class AddExtraBuilding(val building: ExtraBuildingInfo) : HouseFormEvent + +data class RemoveExtraBuilding(val building: ExtraBuildingInfo) : HouseFormEvent +``` + +3. In `HouseFormState`, add: + +```kotlin +val extraBuildings: List = emptyList(), +``` + +- [ ] **Step 2: Handle the two new events in the villa presenter** + +Inside the villa presenter's `CollectEvents { event -> when (event) { ... } }` block, add: + +```kotlin +is HouseFormEvent.AddExtraBuilding -> { + currentState = currentState.copy(extraBuildings = currentState.extraBuildings + event.building) +} + +is HouseFormEvent.RemoveExtraBuilding -> { + currentState = currentState.copy( + extraBuildings = currentState.extraBuildings.filterNot { it == event.building }, + ) +} +``` + +- [ ] **Step 3: Pass extra buildings into the submit use case** + +In the villa presenter's `LaunchedEffect(submitIteration) { ... }` block, find the call to `submitHouseFormAndGetOffersUseCase.invoke(...)` and add as the final argument: + +```kotlin +extraBuildings = currentState.extraBuildings, +``` + +- [ ] **Step 4: Render `ExtraBuildingsSection` in the villa destination** + +In `HouseFormDestination.kt`: + +1. Add the imports: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingsSection +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.allExtraBuildingTypes +``` + +2. After the existing `isSubleted` field (the last collected form field before the submit button) and before the submit `HedvigButton`, insert: + +```kotlin +Spacer(Modifier.height(16.dp)) +ExtraBuildingsSection( + extraBuildings = uiState.extraBuildings, + allowedExtraBuildings = allExtraBuildingTypes, + onAddBuilding = { onEvent(HouseFormEvent.AddExtraBuilding(it)) }, + onRemoveBuilding = { onEvent(HouseFormEvent.RemoveExtraBuilding(it)) }, + enabled = !uiState.isSubmitting, + modifier = Modifier.fillMaxWidth(), +) +``` + +Add `Spacer`, `Modifier`, `fillMaxWidth`, `height` imports only if not already present. + +- [ ] **Step 5: Add the new parameter to `SubmitHouseFormAndGetOffersUseCase`** + +In `SubmitHouseFormAndGetOffersUseCase.kt`: + +1. Add the import: + +```kotlin +import com.hedvig.android.feature.purchase.house.ui.extrabuildings.ExtraBuildingInfo +``` + +2. Add a parameter `extraBuildings: List` at the end of `invoke(...)`'s parameter list. + +3. Find the placeholder `put("extraBuildings", emptyList>())` line (analogous to the one in `SubmitVacationHomeFormAndGetOffersUseCase`) and replace it with: + +```kotlin +put( + "extraBuildings", + extraBuildings.map { building -> + mapOf( + "type" to building.type, + "area" to building.area, + "hasWaterConnected" to building.hasWaterConnected, + ) + }, +) +``` + +If the file does **not** contain the `put("extraBuildings", emptyList(...))` placeholder (the villa use case may not have been written symmetrically), add a new `put("extraBuildings", ...)` entry to the same map-building block where other fields like `numberOfBathrooms` and `isSubleted` are added. + +- [ ] **Step 6: Format + compile-check** + +Run: `./gradlew :feature-purchase-house:ktlintFormat :feature-purchase-house:compileDebugKotlin --quiet` +Expected: completes without errors. + +- [ ] **Step 7: Commit** + +```bash +git add app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt \ + app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt +git commit -m "feat: wire extra buildings into villa purchase flow" +``` + +--- + +### Task 4: Verify in emulator + +**Files:** none + +- [ ] **Step 1: Invoke the emulator verification skill** + +Use the `verifying-android-changes-in-emulator` skill. This is **required** per repo conventions — Android changes must be verified end-to-end, not only by build/compile. + +- [ ] **Step 2: Manual scenarios to run** + +For each of vacation-home and villa purchase flows: + +1. **Empty-list submission:** Enter all required fields, do **not** add any extra buildings, submit. Verify offers screen appears (no regression). +2. **Add and submit:** Enter all required fields, tap "Add extra building", pick `Garage`, enter `25` for size, toggle water on, tap Save. Verify the row appears under "Extra buildings" showing `Garage` / `25 m² ∙ Water connected`. Submit. Verify offers screen appears. +3. **Add multiple, remove one, submit:** Add `Garage` (25 m², water on), add `Shed` (10 m², water off). Tap the close button on the `Garage` row — verify only `Shed` remains. Submit. Verify offers screen appears. +4. **Dialog cancel:** Tap "Add extra building", do not pick anything, tap Cancel — verify dialog closes and no row is added. +5. **Dialog dismiss without size:** Pick `Garage`, leave size empty, tap Save — verify the size field shows the error state and the row is not added. +6. **Repeat all five** for the villa form. + +- [ ] **Step 3: Confirm the offers reflect the buildings** + +After a submission with one or more extra buildings, compare the displayed offer price against a submission of the same form data **without** extra buildings. The price should differ (the backend uses extra buildings in pricing). If the prices are identical, that is a signal the serialization didn't reach the backend correctly — re-check the `put("extraBuildings", ...)` blocks in both use cases. + +- [ ] **Step 4: Run ktlint + module build once more before opening a PR** + +Run: `./gradlew :feature-purchase-house:ktlintCheck :feature-purchase-house:assembleDebug --quiet` +Expected: completes without errors. + +--- + +## Out of scope (do not implement) + +- Sharing the section with `feature-movingflow` (would require a new `ui-extra-buildings` module). Defer until either UI needs to change in lockstep or a third consumer appears. +- Adding `extraBuildingTypesV2` to `PriceIntent` in the backend. +- Edit-in-place affordance on rows. +- New Lokalise keys (TODO comments only — translation strings ship in a follow-up PR once Lokalise is updated). diff --git a/docs/superpowers/specs/2026-05-22-extra-buildings-in-house-purchase-design.md b/docs/superpowers/specs/2026-05-22-extra-buildings-in-house-purchase-design.md new file mode 100644 index 0000000000..92921f7e02 --- /dev/null +++ b/docs/superpowers/specs/2026-05-22-extra-buildings-in-house-purchase-design.md @@ -0,0 +1,123 @@ +# Extra buildings in house purchase flows + +## Context + +The web checkout (racoon) collects a list of "extra buildings" (garage, shed, etc.) as part of the price intent for house and vacation-home insurance. The Android in-app purchase flows for the same products (`feature-purchase-house`, screens `VacationHomeFormDestination` and `HouseFormDestination`) currently submit `extraBuildings = emptyList()` as a placeholder, so users on Android cannot influence the price by declaring outbuildings. + +The Android move flow (`feature-movingflow`) already has a working extra-buildings UI inside `AddHouseInformationDestination.kt`. We will copy that UI into the purchase module rather than extract a shared module — extraction is deferred until there is a clear third caller. + +This spec covers the in-app **purchase** flows only (the buy flow). The move flow is untouched. + +## Goal + +Allow the user to add and remove extra buildings during the vacation-home and villa (SE_HOUSE) purchase forms, and submit them as part of the existing `priceIntentDataUpdate` mutation so that returned offers reflect the declared buildings. + +## Non-goals + +- No backend changes. We do **not** add `extraBuildingTypesV2` to `PriceIntent`. The 14 building types are hardcoded client-side, matching racoon's per-template approach. +- No incremental/live mutation per add/remove. The list is collected locally and submitted in the existing batched `SubmitForm` mutation. +- No edit affordance on existing items. Users add and remove only — matching racoon and the move flow. +- No shared `ui-extra-buildings` module. UI is copied into `feature-purchase-house`. +- No moving-flow refactor. + +## Locked design decisions + +| # | Decision | Choice | +|---|----------|--------| +| 1 | Where shared extra-buildings code lives | Inside `feature-purchase-house`, in a new `ui/extrabuildings/` package, consumed by both purchase forms | +| 2 | Submission timing | Batch — included in the existing form-submit `priceIntentDataUpdate` mutation | +| 3 | UI surface for adding | `HedvigDialog` (copied from move flow, which uses dialog not bottom sheet) | +| 4 | Type list source | Hardcoded client-side list of 14 `MoveExtraBuildingType` entries with Lokalise display names | +| 5 | Edit behavior | Add and remove only; no edit | +| 6 | Code source | Copy from `feature-movingflow` (Path 2), not extract | + +## Files changed + +### New + +`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/extrabuildings/ExtraBuildingsSection.kt` + +Contains, all `internal`: + +- `data class ExtraBuildingInfo(val area: Int, val type: String, val displayName: String, val hasWaterConnected: Boolean)` +- `data class MoveExtraBuildingType(val type: String, val displayName: String)` +- `@Composable fun rememberAllExtraBuildingTypes(): List` — returns the 14 hardcoded entries, with `displayName` resolved via `stringResource(...)`. Wrapped in `remember` keyed on configuration so translations refresh on locale change. +- `@Composable fun ExtraBuildingsSection(extraBuildings: List, allowedExtraBuildings: List, onAddBuilding: (ExtraBuildingInfo) -> Unit, onRemoveBuilding: (ExtraBuildingInfo) -> Unit, enabled: Boolean, modifier: Modifier = Modifier)` — copied from `ExtraBuildingsCard`, with the `ListInput` parameter replaced by plain list + callbacks. `enabled` mirrors the move-flow's `shouldDisableInput` semantics but inverted (true = interactive). +- `@Composable private fun AddExtraBuildingDialogContent(...)` — copied from `ExtraBuildingsDialogContent`, with the same parameter substitution. + +### Modified + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormViewModel.kt`** + +- `VacationHomeFormState` gains `val extraBuildings: List = emptyList()`. +- `VacationHomeFormEvent` gains: + - `data class AddExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent` + - `data class RemoveExtraBuilding(val building: ExtraBuildingInfo) : VacationHomeFormEvent` +- `CollectEvents { ... }` handles the two new events with `currentState.copy(extraBuildings = currentState.extraBuildings + event.building)` and `currentState.copy(extraBuildings = currentState.extraBuildings.filterNot { it == event.building })`. +- The `SubmitForm` branch reads `currentState.extraBuildings` and passes it to the use-case call. + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/vacationhome/VacationHomeFormDestination.kt`** + +- Render `ExtraBuildingsSection(...)` after the `isSubleted` field, before the submit button. Wire `onAddBuilding`/`onRemoveBuilding` to emit the new events. Build `allowedExtraBuildings` via `rememberAllExtraBuildingTypes()`. + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormViewModel.kt`** (lives on `feat/in-app-house-purchase`) + +Same shape of changes as `VacationHomeFormViewModel.kt`. + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/ui/house/HouseFormDestination.kt`** (lives on `feat/in-app-house-purchase`) + +Same shape of changes as `VacationHomeFormDestination.kt`. + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitVacationHomeFormAndGetOffersUseCase.kt`** + +- Add parameter: `extraBuildings: List`. +- Replace the placeholder `put("extraBuildings", emptyList>())` (currently around line 59) with: + + ```kotlin + put("extraBuildings", extraBuildings.map { + mapOf( + "type" to it.type, + "area" to it.area, + "hasWaterConnected" to it.hasWaterConnected, + ) + }) + ``` + +**`app/feature/feature-purchase-house/src/main/kotlin/com/hedvig/android/feature/purchase/house/data/SubmitHouseFormAndGetOffersUseCase.kt`** (lives on `feat/in-app-house-purchase`) + +Same shape of changes as `SubmitVacationHomeFormAndGetOffersUseCase.kt`. + +## Data model + +`ExtraBuildingInfo` is the in-app value object. `displayName` is client-side only and is never serialised to the backend — only `type`, `area`, and `hasWaterConnected` are sent. + +`MoveExtraBuildingType` is a UI helper pairing the canonical type string with a translated display name. The canonical strings mirror the `MoveExtraBuildingType` GraphQL enum: `GARAGE`, `CARPORT`, `SHED`, `STOREHOUSE`, `FRIGGEBOD`, `ATTEFALL`, `OUTHOUSE`, `GUESTHOUSE`, `GAZEBO`, `GREENHOUSE`, `SAUNA`, `BARN`, `BOATHOUSE`, `OTHER`. + +## Lokalise + +Reuse the existing `CHANGE_ADDRESS_EXTRA_BUILDINGS_*` keys for the section title, "Add" button, water label, size label, and bottom-sheet title — the UI text is identical between the move and purchase contexts, and renaming would require a Lokalise round-trip with no UX benefit. + +For the 14 building-type display names: if existing Lokalise keys cover them (e.g. via the move flow), reuse those keys. If any are missing, stub with `// TODO: Add "" / "" to Lokalise` comments mirroring the existing TODO pattern in `VacationHomeFormViewModel.kt`. + +## Validation + +- The extra-buildings list is optional. Empty list is valid; no submission-time validation is added. +- Inside the add-dialog (copied as-is from the move flow): the user must pick a type before "Save" enables. Missing area shows the existing `WithoutMessage` error state. + +## Branching + +This worktree (`claude/amazing-murdock-160a34`) is based on `feat/in-app-vacation-home-purchase`, which contains only the vacation-home form. The villa (`HouseForm*`) files live on `feat/in-app-house-purchase`, stacked on top. + +The implementation branch should stack on `feat/in-app-house-purchase` so both forms can be modified in one PR. If kept on the vacation-home branch, the villa changes would have to land in a follow-up PR after rebase. + +## Test plan + +- Manual: open the vacation-home purchase flow, add two buildings of different types with water connected/not, remove one, submit. Verify offers reflect the data. Repeat for villa. +- Manual: submit with no extra buildings — verify the existing batched flow behaves unchanged. +- Existing unit tests (if any) for the form view models continue to pass; if they assert on state shape, extend them to cover the new `extraBuildings` field. + +## Out of scope / follow-ups + +- Sharing the UI with the move flow (extract to a `ui-extra-buildings` module). Defer until either UI needs to change in lockstep, or a third consumer appears. +- Server-driven type list on `PriceIntent` (would require backend work to add `extraBuildingTypesV2` there). +- Edit-in-place affordance on existing rows.