Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions app/app/src/main/kotlin/com/hedvig/android/app/ui/HedvigApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.scene.DialogSceneStrategy
import androidx.navigation3.scene.Scene
import androidx.navigation3.scene.SinglePaneSceneStrategy
import androidx.navigation3.ui.NavDisplay
import coil3.ImageLoader
import com.hedvig.android.app.AndroidAppHost
Expand Down Expand Up @@ -70,6 +72,7 @@ import com.hedvig.android.language.LanguageService
import com.hedvig.android.logger.logcat
import com.hedvig.android.navigation.activity.ExternalNavigator
import com.hedvig.android.navigation.common.HedvigNavKey
import com.hedvig.android.navigation.compose.BottomSheetSceneStrategy
import com.hedvig.android.navigation.compose.HedvigDeepLinkMatcher
import com.hedvig.android.navigation.compose.entryDecorators
import com.hedvig.android.notification.badge.data.payment.MissedPaymentNotificationService
Expand Down Expand Up @@ -162,6 +165,13 @@ internal fun HedvigApp(
hedvigAppState = hedvigAppState,
sharedTransitionScope = this@SharedTransitionLayout,
)
val sceneStrategies = remember {
listOf(
BottomSheetSceneStrategy<HedvigNavKey>(),
DialogSceneStrategy<HedvigNavKey>(),
SinglePaneSceneStrategy(),
)
}
val density = LocalDensity.current
val popSpec = hedvigPopTransitionSpec(backstackController, density)
// Hold the first frame on the themed background until SessionReconciler resolves the start
Expand All @@ -182,6 +192,7 @@ internal fun HedvigApp(
onBack = backstackController::popBackstack,
entryDecorators = entryDecorators { backstackController.allLiveContentKeys },
sharedTransitionScope = this@SharedTransitionLayout,
sceneStrategies = sceneStrategies,
sceneDecoratorStrategies = sceneDecoratorStrategies,
transitionSpec = hedvigTransitionSpec(backstackController, density),
popTransitionSpec = popSpec,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ internal class ExternalDeepLinkHandler(
return
}
if (targetsOwnDeepLink(uri)) {
logcat(LogPriority.WARN) { "ExternalDeepLinkHandler no specific match for own-host uri:$uri — falling back to Home" }
logcat(LogPriority.WARN) {
"ExternalDeepLinkHandler no specific match for own-host uri:$uri — falling back to Home"
}
backstackController.navigateToExternalDeepLink(HomeKey)
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.hedvig.android.app.navigation

import androidx.navigation3.runtime.NavEntry
import androidx.navigation3.scene.OverlayScene
import androidx.navigation3.scene.SceneStrategyScope
import assertk.assertThat
import assertk.assertions.isNotEmpty
import assertk.assertions.isNull
import assertk.assertions.isTrue
import com.hedvig.android.navigation.compose.BottomSheetSceneStrategy
import org.junit.Test

internal class BottomSheetSceneStrategyTest {
private fun strategy() = BottomSheetSceneStrategy<String>()

private fun entry(key: String, metadata: Map<String, Any> = emptyMap()) = NavEntry(key = key, metadata = metadata) { }

@Test
fun `bottomSheet metadata is recognized as a bottom-sheet marker`() {
assertThat(BottomSheetSceneStrategy.bottomSheet().keys).isNotEmpty()
}

@Test
fun `calculateScene returns null when the top entry has no bottomSheet metadata`() {
val entries = listOf(entry("a"), entry("b"))
val scene = with(strategy()) { with(SceneStrategyScope<String>()) { calculateScene(entries) } }
assertThat(scene).isNull()
}

@Test
fun `calculateScene returns an overlay scene that overlays the entries below the sheet`() {
val entries = listOf(entry("a"), entry("b", BottomSheetSceneStrategy.bottomSheet()))
val scene = with(strategy()) { with(SceneStrategyScope<String>()) { calculateScene(entries) } }
assertThat(scene is OverlayScene<String>).isTrue()
val overlay = scene as OverlayScene<String>
assertThat(overlay.overlaidEntries.map { it.contentKey } == listOf<Any>("a")).isTrue()
assertThat(overlay.entries.map { it.contentKey } == listOf<Any>("b")).isTrue()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.hedvig.android.auth.AuthTokenServiceImpl
import com.hedvig.android.auth.event.AuthEventStorage
import com.hedvig.android.auth.storage.AuthTokenStorage
import com.hedvig.android.auth.test.FakeAuthRepository
import com.hedvig.android.core.common.ApplicationScope
import com.hedvig.android.core.datastore.TestPreferencesDataStore
import com.hedvig.android.logger.TestLogcatLoggingRule
import com.hedvig.android.test.clock.TestClock
Expand Down Expand Up @@ -188,7 +189,7 @@ class AndroidAccessTokenProviderTest {
authTokenStorage,
fakeAuthRepository,
AuthEventStorage(),
backgroundScope,
ApplicationScope(backgroundScope),
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.hedvig.android.design.system.hedvig.api

import androidx.compose.runtime.Stable

/**
* Drives the hide animation of a presence-driven overlay bottom sheet (one whose visibility is the
* presence of a back-stack entry, not an imperative state flag). The renderer binds the underlying
* material sheet state; callers (e.g. a navigation scene's removal hook) call [hide] to animate the
* sheet closed before the entry leaves composition.
*/
@Stable
interface HedvigOverlaySheetController {
suspend fun hide()
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ private fun <T> InternalHedvigBottomSheet(
}

@Composable
private fun DragHandle(modifier: Modifier = Modifier) {
internal fun DragHandle(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.width(40.dp)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.hedvig.android.design.system.hedvig

import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.hedvig.android.design.system.hedvig.api.HedvigOverlaySheetController
import com.hedvig.android.design.system.internals.OverlayBottomSheet
import com.hedvig.android.design.system.internals.rememberOverlaySheetController

/**
* A presence-driven Hedvig bottom sheet for content whose visibility is owned by the caller
* (e.g. a navigation back-stack entry) rather than a
* [com.hedvig.android.design.system.hedvig.api.HedvigBottomSheetState]. Styled identically
* to [HedvigBottomSheet]: BackgroundPrimary container, TextPrimary content, Scrim @ 32 %,
* CornerXLargeTop shape, the Hedvig drag handle, and 16 dp horizontal content padding.
*
* Animate it closed with [controller].hide() before the content leaves the composition.
*/
@Composable
fun HedvigOverlayBottomSheet(
controller: HedvigOverlaySheetController,
onDismissRequest: () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
OverlayBottomSheet(
controller = controller,
onDismissRequest = onDismissRequest,
containerColor = bottomSheetColors.bottomSheetBackgroundColor,
contentColor = bottomSheetColors.contentColor,
scrimColor = bottomSheetColors.scrimColor,
shape = bottomSheetShape.shape,
contentPadding = PaddingValues(horizontal = 16.dp),
dragHandle = {
DragHandle(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
.padding(top = 8.dp, bottom = 20.dp),
)
},
content = content,
)
}

@Composable
fun rememberHedvigOverlaySheetController(): HedvigOverlaySheetController = rememberOverlaySheetController()
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.hedvig.android.design.system.internals

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import com.hedvig.android.design.system.hedvig.api.HedvigOverlaySheetController

/**
* A presence-driven [ModalBottomSheet]: it is shown for as long as it is composed, and animated
* closed via [HedvigOverlaySheetController.hide]. Unlike [BottomSheet], it has no visibility/data
* state of its own — the caller controls its lifetime (e.g. a navigation back-stack entry).
*
* Styling is passed in as plain values; `design-system-hedvig` supplies the Hedvig tokens.
*/
@Composable
fun OverlayBottomSheet(
controller: HedvigOverlaySheetController,
onDismissRequest: () -> Unit,
containerColor: Color,
contentColor: Color,
scrimColor: Color,
shape: Shape,
contentPadding: PaddingValues,
dragHandle: @Composable () -> Unit,
content: @Composable ColumnScope.() -> Unit,
) {
check(controller is OverlaySheetControllerImpl) {
"Expected OverlaySheetControllerImpl, got ${controller::class}. Use rememberOverlaySheetController()."
}
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = controller.materialState,
containerColor = containerColor,
contentColor = contentColor,
scrimColor = scrimColor,
shape = shape,
dragHandle = dragHandle,
) {
Column(Modifier.padding(contentPadding)) {
content()
}
}
}

@Composable
fun rememberOverlaySheetController(): HedvigOverlaySheetController {
val materialState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
return remember(materialState) { OverlaySheetControllerImpl(materialState) }
}

private class OverlaySheetControllerImpl(
val materialState: SheetState,
) : HedvigOverlaySheetController {
override suspend fun hide() {
materialState.hide()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ internal data object ManualChargeKey : HedvigNavKey
internal data class ManualChargeSuccessKey(
val showCancellationWarning: Boolean,
) : HedvigNavKey

@Serializable
internal data class PaymentDetailExplanationKey(
val title: String,
val body: String,
) : HedvigNavKey
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.hedvig.android.feature.payments.navigation
import androidx.lifecycle.compose.dropUnlessResumed
import androidx.navigation3.runtime.EntryProviderScope
import com.hedvig.android.compose.ui.dropUnlessResumed
import com.hedvig.android.feature.payments.ui.details.PaymentDetailExplanationContent
import com.hedvig.android.feature.payments.ui.details.PaymentDetailsDestination
import com.hedvig.android.feature.payments.ui.details.PaymentDetailsViewModel
import com.hedvig.android.feature.payments.ui.details.PaymentDetailsViewModelFactory
Expand All @@ -19,6 +20,7 @@ import com.hedvig.android.feature.payments.ui.payments.PaymentsDestination
import com.hedvig.android.feature.payments.ui.payments.PaymentsViewModel
import com.hedvig.android.navigation.common.HedvigNavKey
import com.hedvig.android.navigation.compose.Backstack
import com.hedvig.android.navigation.compose.BottomSheetSceneStrategy
import com.hedvig.android.navigation.compose.NavSuiteSceneDecoratorStrategy
import com.hedvig.android.navigation.compose.add
import com.hedvig.android.shared.foreverui.ui.ui.ForeverDestination
Expand Down Expand Up @@ -82,6 +84,17 @@ fun EntryProviderScope<HedvigNavKey>.paymentsEntries(
PaymentDetailsDestination(
viewModel = viewModel,
navigateUp = backstack::navigateUp,
onShowExplanation = dropUnlessResumed { title: String, body: String ->
backstack.add(PaymentDetailExplanationKey(title, body))
},
)
}

entry<PaymentDetailExplanationKey>(metadata = BottomSheetSceneStrategy.bottomSheet()) { key ->
PaymentDetailExplanationContent(
title = key.title,
body = key.body,
onClose = backstack::popBackstack,
)
}

Expand Down
Loading
Loading