Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
de1fa92
chore: add design spec for in-app pet purchase flow
hugokallstrom May 21, 2026
6bddf1f
chore: add implementation plan for in-app pet purchase flow
hugokallstrom May 21, 2026
e9b6192
feat: scaffold feature-purchase-pet module
hugokallstrom May 21, 2026
2446d43
feat: add pet purchase GraphQL operations
hugokallstrom May 21, 2026
91fe43d
feat: add pet purchase domain models
hugokallstrom May 21, 2026
2208858
feat: add CreatePetSessionAndPriceIntentUseCase
hugokallstrom May 21, 2026
800af5a
feat: add GetPetBreedsUseCase
hugokallstrom May 21, 2026
bd637de
feat: add SubmitPetFormAndGetOffersUseCase
hugokallstrom May 21, 2026
051af67
feat: add pet purchase navigation destinations
hugokallstrom May 21, 2026
04179ac
feat: add PetFormViewModel and presenter
hugokallstrom May 21, 2026
6cc4ea4
feat: add PetFormDestination UI
hugokallstrom May 21, 2026
babc4b3
feat: add petPurchaseNavGraph
hugokallstrom May 21, 2026
7155645
feat: add petPurchaseModule Koin DI
hugokallstrom May 21, 2026
b0f0424
feat: register petPurchaseModule in ApplicationModule
hugokallstrom May 21, 2026
4c7ac1f
feat: integrate pet purchase flow into navigation and cross-sell routing
hugokallstrom May 21, 2026
851a55d
chore: ktlint format
hugokallstrom May 21, 2026
7971291
test: add missing SubmitPetFormAndGetOffersUseCase test cases
hugokallstrom May 21, 2026
d44ea3b
feat: add species picker and switch cross-sell URL match to pet-insur…
hugokallstrom May 21, 2026
f1e8b1c
chore: design-system tweaks for purchase forms
hugokallstrom May 21, 2026
488200d
feat: pet form UX polish
hugokallstrom May 21, 2026
18e2efb
chore: add lint baselines for feature-purchase-pet and feature-purcha…
hugokallstrom May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ dependencies {
implementation(projects.featureAddonPurchase)
implementation(projects.featurePurchaseApartment)
implementation(projects.featurePurchaseCar)
implementation(projects.featurePurchasePet)
implementation(projects.purchaseCommon)
implementation(projects.featureChat)
implementation(projects.featureChooseTier)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import com.hedvig.android.feature.profile.di.profileModule
import com.hedvig.android.feature.purchase.apartment.di.apartmentPurchaseModule
import com.hedvig.android.feature.purchase.car.di.carPurchaseModule
import com.hedvig.android.feature.purchase.common.di.purchaseCommonModule
import com.hedvig.android.feature.purchase.pet.di.petPurchaseModule
import com.hedvig.android.feature.terminateinsurance.di.terminateInsuranceModule
import com.hedvig.android.feature.travelcertificate.di.travelCertificateModule
import com.hedvig.android.featureflags.di.featureManagerModule
Expand Down Expand Up @@ -295,6 +296,7 @@ val applicationModule = module {
addonRemovalModule,
apartmentPurchaseModule,
carPurchaseModule,
petPurchaseModule,
androidPermissionModule,
apolloAuthListenersModule,
appModule,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import com.hedvig.android.feature.purchase.car.navigation.CarPurchaseGraphDestin
import com.hedvig.android.feature.purchase.car.navigation.carPurchaseNavGraph
import com.hedvig.android.feature.purchase.common.navigation.PurchaseCommonDestination
import com.hedvig.android.feature.purchase.common.ui.success.PurchaseSuccessDestination
import com.hedvig.android.feature.purchase.pet.navigation.PetPurchaseGraphDestination
import com.hedvig.android.feature.purchase.pet.navigation.petPurchaseNavGraph
import com.hedvig.android.feature.terminateinsurance.navigation.TerminateInsuranceGraphDestination
import com.hedvig.android.feature.terminateinsurance.navigation.terminateInsuranceGraph
import com.hedvig.android.feature.travelcertificate.navigation.TravelCertificateGraphDestination
Expand Down Expand Up @@ -337,6 +339,9 @@ internal fun HedvigNavHost(
onNavigateToCarPurchase = { productName ->
navController.navigate(CarPurchaseGraphDestination(productName))
},
onNavigateToPetPurchase = {
navController.navigate(PetPurchaseGraphDestination)
},
)
foreverGraph(
hedvigDeepLinkContainer = hedvigDeepLinkContainer,
Expand Down Expand Up @@ -504,6 +509,12 @@ internal fun HedvigNavHost(
finishApp = finishApp,
crossSellAfterFlowRepository = crossSellAfterFlowRepository,
)
petPurchaseNavGraph(
navController = navController,
popBackStack = popBackStackOrFinish,
finishApp = finishApp,
crossSellAfterFlowRepository = crossSellAfterFlowRepository,
)
navdestination<PurchaseCommonDestination.Success> { backStackEntry ->
val route = backStackEntry.toRoute<PurchaseCommonDestination.Success>()
PurchaseSuccessDestination(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ fun DropdownWithDialog(
color = dropdownColors.containerColor(false).value,
shape = size.shape,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
style.items.forEachIndexed { index, item ->
DropdownOption(
item = item,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private fun RadioGroup(
) {
Box(modifier) {
if (style.style is RadioGroupStyle.Labeled) {
RadioSurface(style, colors) {
RadioSurface(style, colors, modifier = Modifier.fillMaxWidth()) {
Column {
HedvigText(
text = style.style.label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ fun NavGraphBuilder.insuranceGraph(
navigateToUpgradeAddon: (ContractId?, AddonVariant?) -> Unit,
onNavigateToApartmentPurchase: (productName: String) -> Unit,
onNavigateToCarPurchase: (productName: String) -> Unit,
onNavigateToPetPurchase: () -> Unit,
) {
navgraph<InsurancesDestination.Graph>(
startDestination = InsurancesDestination.Insurances::class,
Expand Down Expand Up @@ -70,13 +71,25 @@ fun NavGraphBuilder.insuranceGraph(
}
val lower = decoded.lowercase()
when {
"car-insurance" in lower || "bilforsakring" in lower ->
"car-insurance" in lower || "bilforsakring" in lower -> {
onNavigateToCarPurchase("SE_CAR")
"bostadsratt" in lower || "home-insurance/homeowner" in lower ->
}

"pet-insurance" in lower || "djurforsakring" in lower -> {
onNavigateToPetPurchase()
}

"bostadsratt" in lower || "home-insurance/homeowner" in lower -> {
onNavigateToApartmentPurchase("SE_APARTMENT_BRF")
"hyresratt" in lower || "home-insurance" in lower || "hemforsakring" in lower ->
}

"hyresratt" in lower || "home-insurance" in lower || "hemforsakring" in lower -> {
onNavigateToApartmentPurchase("SE_APARTMENT_RENT")
else -> openUrl(url)
}

else -> {
openUrl(url)
}
}
},
navigateToCancelledInsurances = dropUnlessResumed {
Expand Down Expand Up @@ -136,4 +149,3 @@ fun NavGraphBuilder.insuranceGraph(
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,7 @@ private data class ValidationErrors(
zipCodeError != null
}

private fun validate(
registrationNumber: String,
mileage: Int?,
street: String,
zipCode: String,
): ValidationErrors {
private fun validate(registrationNumber: String, mileage: Int?, street: String, zipCode: String): ValidationErrors {
return ValidationErrors(
registrationNumberError = when {
registrationNumber.isBlank() -> "Ange registreringsnummer"
Expand Down
52 changes: 52 additions & 0 deletions app/feature/feature-purchase-pet/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
plugins {
id("hedvig.android.library")
id("hedvig.gradle.plugin")
}

hedvig {
apollo("octopus")
serialization()
compose()
}

android {
testOptions.unitTests.isReturnDefaultValues = true
}

dependencies {
api(libs.androidx.navigation.common)

implementation(libs.androidx.navigation.compose)
implementation(libs.arrow.core)
implementation(libs.arrow.fx)
implementation(libs.jetbrains.lifecycle.runtime.compose)
implementation(libs.koin.composeViewModel)
implementation(libs.koin.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.core)
implementation(projects.apolloCore)
implementation(projects.apolloOctopusPublic)
implementation(projects.composeUi)
implementation(projects.coreCommonPublic)
implementation(projects.coreResources)
implementation(projects.coreUiData)
implementation(projects.dataCrossSellAfterFlow)
implementation(projects.designSystemHedvig)
implementation(projects.purchaseCommon)
implementation(projects.moleculePublic)
implementation(projects.navigationCommon)
implementation(projects.navigationCompose)
implementation(projects.navigationComposeTyped)
implementation(projects.navigationCore)

testImplementation(libs.apollo.testingSupport)
testImplementation(libs.assertK)
testImplementation(libs.coroutines.test)
testImplementation(libs.junit)
testImplementation(libs.turbine)
testImplementation(projects.apolloOctopusTest)
testImplementation(projects.apolloTest)
testImplementation(projects.coreCommonTest)
testImplementation(projects.loggingTest)
testImplementation(projects.moleculeTest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query PetAvailableBreeds($animal: PriceIntentAnimal!) {
priceIntentAvailableBreeds(animal: $animal) {
id
displayName
isMixedBreed
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
query PetMemberContactInfo {
currentMember {
id
ssn
email
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
mutation PetPriceIntentConfirm($priceIntentId: UUID!) {
priceIntentConfirm(priceIntentId: $priceIntentId) {
priceIntent {
id
offers {
...PetProductOfferFragment
}
}
userError {
message
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation PetPriceIntentCreate($shopSessionId: UUID!, $productName: String!) {
priceIntentCreate(input: { shopSessionId: $shopSessionId, productName: $productName }) {
id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
mutation PetPriceIntentDataUpdate($priceIntentId: UUID!, $data: PricingFormData!) {
priceIntentDataUpdate(priceIntentId: $priceIntentId, data: $data) {
priceIntent {
id
}
userError {
message
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
fragment PetProductOfferFragment on ProductOffer {
id
variant {
displayName
displayNameSubtype
displayNameTier
tierDescription
typeOfContract
perils {
title
description
colorCode
covered
info
}
documents {
type
displayName
url
}
}
cost {
gross {
...MoneyFragment
}
net {
...MoneyFragment
}
discountsV2 {
amount {
...MoneyFragment
}
}
}
startDate
deductible {
displayName
amount
}
usps
exposure {
displayNameShort
}
bundleDiscount {
isEligible
potentialYearlySavings {
...MoneyFragment
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mutation PetShopSessionCreate($countryCode: CountryCode!) {
shopSessionCreate(input: { countryCode: $countryCode }) {
id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.hedvig.android.feature.purchase.pet.data

import arrow.core.Either
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.logger.LogPriority
import com.hedvig.android.logger.logcat
import octopus.PetMemberContactInfoQuery
import octopus.PetPriceIntentCreateMutation
import octopus.PetShopSessionCreateMutation
import octopus.type.CountryCode

internal interface CreatePetSessionAndPriceIntentUseCase {
suspend fun invoke(productName: String): Either<ErrorMessage, SessionAndIntent>
}

internal class CreatePetSessionAndPriceIntentUseCaseImpl(
private val apolloClient: ApolloClient,
) : CreatePetSessionAndPriceIntentUseCase {
override suspend fun invoke(productName: String): Either<ErrorMessage, SessionAndIntent> {
return either {
val shopSessionId = apolloClient
.mutation(PetShopSessionCreateMutation(CountryCode.SE))
.safeExecute()
.fold(
ifLeft = {
logcat(LogPriority.ERROR) { "Failed to create shop session: $it" }
raise(ErrorMessage())
},
ifRight = { it.shopSessionCreate.id },
)

val priceIntentId = apolloClient
.mutation(PetPriceIntentCreateMutation(shopSessionId = shopSessionId, productName = productName))
.safeExecute()
.fold(
ifLeft = {
logcat(LogPriority.ERROR) { "Failed to create price intent: $it" }
raise(ErrorMessage())
},
ifRight = { it.priceIntentCreate.id },
)

val member = apolloClient
.query(PetMemberContactInfoQuery())
.safeExecute()
.fold(
ifLeft = {
logcat(LogPriority.ERROR) { "Failed to fetch member contact info: $it" }
raise(ErrorMessage())
},
ifRight = { it.currentMember },
)
val ssn = member.ssn
if (ssn == null) {
logcat(LogPriority.ERROR) { "Member is missing SSN — cannot continue pet purchase" }
raise(ErrorMessage())
}

SessionAndIntent(
shopSessionId = shopSessionId,
priceIntentId = priceIntentId,
ssn = ssn,
email = member.email,
)
}
}
}
Loading
Loading