Add support for JSConfirm dialog in FrontendScreen#6749
Conversation
There was a problem hiding this comment.
Pull request overview
Adds initial support for handling JavaScript confirm() dialogs in the Compose-based FrontendScreen flow, wiring WebView callbacks through the FrontendViewModel into a Compose dialog surface and updating theme tokens to match Material3 dialog styling.
Changes:
- Extend
HAWebChromeClientandFrontendViewModelto interceptonJsConfirmand surface it aspendingDialoginFrontendViewState.Content - Introduce a small dialog model/renderer (
FrontendDialog,PendingDialogHandler,SimpleConfirmDialog) and render it fromFrontendScreenContent - Add unit/UI tests plus screenshot test coverage for the new dialog UI state
Reviewed changes
Copilot reviewed 11 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| common/src/main/kotlin/io/homeassistant/companion/android/common/compose/theme/HATheme.kt | Sets surfaceContainerHigh to ensure Material3 AlertDialog container color matches HA surface |
| app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebChromeClient.kt | Adds onJsConfirm callback plumbing from WebView into app code |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt | Creates FrontendDialog.Confirm from JS confirms and stores it in view state |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewState.kt | Adds pendingDialog field to the Content view state |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt | Renders pending dialogs via PendingDialogHandler in the Compose screen |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/dialog/FrontendDialog.kt | Defines dialog model for frontend-triggered dialogs |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/dialog/PendingDialogHandler.kt | Routes dialog model to a concrete dialog composable |
| app/src/main/kotlin/io/homeassistant/companion/android/frontend/dialog/SimpleConfirmDialog.kt | Implements the confirm dialog UI using AlertDialog + HA buttons/styles |
| app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebChromeClientTest.kt | Unit tests for new HAWebChromeClient.onJsConfirm behavior |
| app/src/test/kotlin/io/homeassistant/companion/android/frontend/dialog/SimpleConfirmDialogTest.kt | Robolectric Compose UI tests for confirm dialog rendering and callbacks |
| app/src/screenshotTest/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest.kt | Adds screenshot preview for FrontendScreenContent with confirm dialog |
| app/src/screenshotTestFullDebug/reference/...png | New golden images for the confirm dialog screenshot tests |
| internal fun SimpleConfirmDialog(pendingDialog: FrontendDialog.Confirm) { | ||
| AlertDialog( | ||
| onDismissRequest = pendingDialog.onCancel, | ||
| title = { Text(text = stringResource(commonR.string.app_name), style = HATextStyle.Headline) }, |
There was a problem hiding this comment.
Looking at the screenshots, the title looks too big compared to the content IMO. Compare with Chrome showing a dialog on Android which is also M3 themed.
jpelgrom
left a comment
There was a problem hiding this comment.
Tested and confirmed working (although it previously already did dialogs from the view world, so I guess 'not broken' is more correct).
| /** | ||
| * Single-slot holder that queues permission requests. | ||
| * | ||
| * [emit] suspends until the slot is free (value is `null`), ensuring only one permission | ||
| * request is active at a time. [clear] frees the slot and unblocks the next waiting caller. | ||
| */ | ||
| @OptIn(ExperimentalForInheritanceCoroutinesApi::class) | ||
| private class PendingPermissionManager( | ||
| private val _state: MutableStateFlow<PermissionRequest<*>?> = MutableStateFlow(null), | ||
| ) : StateFlow<PermissionRequest<*>?> by _state.asStateFlow() { | ||
| private val mutex = Mutex() | ||
| suspend fun emit(request: PermissionRequest<*>) { | ||
| mutex.withLock { | ||
| // Wait for the slot to be free, then fill it atomically. | ||
| // The mutex ensures only one caller can observe null and set a value, | ||
| // preventing concurrent emitters from overwriting each other. | ||
| _state.first { it == null } | ||
| _state.value = request | ||
| } | ||
| } | ||
|
|
||
| fun clear() { | ||
| _state.value = null | ||
| } | ||
| } | ||
|
|
||
| private val _pendingPermissionRequest = PendingPermissionManager() |

Summary
This PR introduced the first dialog of the FrontendScreen. I made a first iteration of the architecture but it is most probably going to change further we add dialogs to fit the requirements.
Checklist
Screenshots
Any other notes
I used this code in a iFrame to test