# ComposeWebView

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![API](https://img.shields.io/badge/API-24%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=24)
[![Kotlin](https://img.shields.io/badge/Kotlin-2.2.0-blue.svg?style=flat&logo=kotlin)](https://kotlinlang.org)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.parkwoocheol/compose-webview.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.parkwoocheol/compose-webview)
[![Compose Multiplatform](https://img.shields.io/badge/Compose%20Multiplatform-1.9.3-blue.svg)](https://github.com/JetBrains/compose-multiplatform)
[![Documentation](https://img.shields.io/badge/docs-website-blueviolet)](https://parkwoocheol.github.io/compose-webview/)

A powerful, flexible, and feature-rich WebView wrapper for **Jetpack Compose** and **Compose Multiplatform** (Android, iOS, Desktop, Web).

 Supports **Android**, **iOS**, **Desktop (JVM)**, and **Web (JS)** with a unified API.

## Table of Contents

- [Documentation](#documentation)
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Usage](#usage)
  - [Basic Usage](#basic-usage)
  - [WebView State and Controller](#webview-state-and-controller)
  - [JSBridge - Kotlin to JavaScript Communication](#jsbridge---kotlin-to-javascript-communication)
  - [Lifecycle Management](#lifecycle-management)
  - [Error Handling](#error-handling)
  - [Custom WebView Configuration](#custom-webview-configuration)
  - [Configuring WebView Clients](#configuring-webview-clients)
- [API Reference](#api-reference)
- [Sample App](#sample-app)
- [Contributing](#contributing)
- [License](#license)

## Documentation

📘 **Full Documentation**: [https://parkwoocheol.github.io/compose-webview/](https://parkwoocheol.github.io/compose-webview/)

Visit the documentation site for comprehensive guides, API references, and advanced usage examples.

## Features

- **Multiplatform Support**: Supports Android, iOS, Desktop (CEF), and Web (JS).
- **Jetpack Compose Integration**: Seamlessly integrates native WebViews with Compose UI.
- **Advanced JSBridge (Mobile Focused)**: A highly productive bridge for Android & iOS.
  - **Promise-based**: JavaScript calls return Promises, allowing `await` syntax (no callback hell).
  - **Type-Safe**: Built-in **Kotlinx Serialization** support automatically converts JSON to Kotlin data classes.
  - **Event Bus**: Bi-directional event system (`emit`/`on`) for real-time communication.
- **State Management**: Reactive state handling for URL, loading progress, and navigation.
- **Flexible API**: Full control over `WebViewClient`, `WebChromeClient`, and WebView settings with convenient composable functions (`rememberWebViewClient`, `rememberWebChromeClient`).
- **Lifecycle Management**: Automatically handles `onResume`, `onPause`, and cleanup.
- **Network Request Interception**: Intercept and provide custom responses for specific URL schemes (Android & iOS).
- **Dark Mode Support**: Automatic theme synchronization with system or manual control (Android & iOS).
- **Enhanced Cookie Management**: Support for removing cookies for specific URLs (Android & iOS).
- **Find on Page**: Built-in support for searching text within pages with result callbacks (Android & iOS).
- **Custom Context Menu**: Override default Android action mode with custom callbacks.
- **Loading & Error States**: Built-in state management for loading indicators and error handling.
- **Back Navigation**: Integrated with Compose `BackHandler` for seamless back navigation (Android) and native swipe gestures (iOS).
- **File Upload**: Full support for file upload functionality (Android & iOS).

## Requirements

- Android API 24+
- iOS 14.0+
- Desktop (JVM) 11+
- Web (JS)
- Jetpack Compose / Compose Multiplatform 1.9.3+
- Kotlin 2.2.0+

## Supported Platforms

 | Platform | Implementation | Status | Note |
 |----------|----------------|--------|------|
 | **Android** | `AndroidView` (WebView) | ✅ Stable | Full feature support |
 | **iOS** | `UIKitView` (WKWebView) | ✅ Stable | Full feature support (Seamless JS Bridge) |
| **Desktop** | `SwingPanel` (CEF via JCEF) | 🚧 Experimental | **WIP**: JCEF integration is in progress. Basic browsing works, but advanced features are still being tested. |
 | **Web (JS)** | `Iframe` (DOM overlay) | 🚧 Experimental | **WIP**: Uses DOM overlay to stay compatible with Compose UI (Canvas). CORS/browser policy limitations apply. |
 | **Web (WASM)** | `Iframe` (DOM) | 🚧 Experimental | **WIP**: Uses iframe with dynamic positioning. Same-origin policy restrictions apply. |

> Web(JS) uses imperative DOM overlay instead of Compose HTML DOM nodes to avoid runtime Applier conflicts in Compose UI(Canvas) apps.

### Desktop (macOS) Troubleshooting

If you see `IllegalAccessError` with `sun.awt`, `sun.lwawt`, or `sun.lwawt.macosx` while running JCEF on macOS, add JVM module options:

```kotlin
compose.desktop {
    application {
        jvmArgs += listOf(
            "--add-exports=java.desktop/sun.awt=ALL-UNNAMED",
            "--add-opens=java.desktop/sun.awt=ALL-UNNAMED",
            "--add-exports=java.desktop/sun.lwawt=ALL-UNNAMED",
            "--add-opens=java.desktop/sun.lwawt=ALL-UNNAMED",
            "--add-exports=java.desktop/sun.lwawt.macosx=ALL-UNNAMED",
            "--add-opens=java.desktop/sun.lwawt.macosx=ALL-UNNAMED",
        )
    }
}
```

### API Support Matrix

<details>
<summary>Click to expand detailed API support by platform</summary>

#### Core Navigation APIs

| API | Android | iOS | Desktop | Web | Notes |
|-----|:-------:|:---:|:-------:|:---:|-------|
| `loadUrl()` | ✅ | ✅ | ✅ | ✅ | Headers: Android/iOS only |
| `loadHtml()` | ✅ | ✅ | ✅ | ⚠️ | Web: CORS restrictions |
| `postUrl()` | ✅ | ✅ | ❌ | ❌ | Desktop/Web not supported |
| `evaluateJavascript()` | ✅ | ✅ | ✅ | ⚠️ | Web: CORS restricted |
| `navigateBack/Forward` | ✅ | ✅ | ✅ | ⚠️ | Web: CORS restricted |
| `reload() / stopLoading()` | ✅ | ✅ | ✅ | ⚠️ | Web: CORS restricted |

#### Control & Navigation

| API | Android | iOS | Desktop | Web | Notes |
|-----|:-------:|:---:|:-------:|:---:|-------|
| `zoomIn/Out/By()` | ✅ | ❌ | ✅ | ❌ | iOS: Pinch-to-zoom only |
| `scrollTo/By()` | ✅ | ✅ | ❌ | ✅ | Desktop not supported |
| `pageUp/Down()` | ✅ | ⚠️ | ❌ | ⚠️ | Via scrollBy on iOS/Web |
| `findAllAsync()` | ✅ | ❌ | ❌ | ❌ | iOS/Web/Desktop not supported |
| `clearCache/History()` | ✅ | ❌ | ❌ | ❌ | Android only |

#### State Management

| Feature | Android | iOS | Desktop | Web | Notes |
|---------|:-------:|:---:|:-------:|:---:|-------|
| `LoadingState` | ✅ | ✅ | ⚠️ | ⚠️ | All states supported on Android/iOS |
| `LoadingState.Loading(progress)` | ✅ | ✅ | ❌ | ❌ | iOS: 100ms polling |
| `ScrollPosition` | ✅ | ✅ | ❌ | ⚠️ | Real-time (Android), Polling (iOS), CORS (Web) |
| `WebViewError` | ✅ | ✅ | ⚠️ | ⚠️ | Typed error categories |
| `jsDialogState` | ✅ | ✅ | ❌ | ❌ | Alert/Confirm/Prompt |
| `customViewState` | ✅ | ⚠️ | ❌ | ❌ | Android: custom view view hierarchy, iOS: native fullscreen signal |

#### Configuration (WebViewSettings)

| Setting | Android | iOS | Desktop | Web | Notes |
|---------|:-------:|:---:|:-------:|:---:|-------|
| `interceptedSchemes` | ✅ | ✅ | ❌ | ❌ | URL schemes to intercept (iOS registration required) |
| `darkMode` | ✅ | ✅ | ❌ | ❌ | DARK, LIGHT, or AUTO theme support |
| `userAgent` | ✅ | ✅ | ✅ | ❌ | Custom user agent string |
| `javaScriptEnabled` | ✅ | ✅* | ✅ | ❌ | *iOS: Always enabled |
| `domStorageEnabled` | ✅ | ✅ | ⚠️ | ❌ | Limited on Desktop |
| `cacheMode` | ✅ | ⚠️ | ⚠️ | ❌ | Full support on Android |
| `supportZoom` | ✅ | ⚠️** | ✅ | ❌ | **iOS: Pinch-to-zoom only |
| `mediaPlaybackRequiresUserAction` | ✅ | ✅ | ⚠️ | ❌ | Autoplay control |

#### Callbacks & Events

| Callback | Android | iOS | Desktop | Web | Notes |
|----------|:-------:|:---:|:-------:|:---:|-------|
| `onPageStarted` | ✅ | ✅ | ✅ | ❌ | Navigation started |
| `onPageFinished` | ✅ | ✅ | ✅ | ✅ | Navigation completed |
| `onProgressChanged` | ✅ | ✅ | ❌ | ❌ | iOS: 100ms polling |
| `onReceivedError` | ✅ | ✅ | ⚠️ | ❌ | Typed error information |
| `shouldInterceptRequest` | ✅ | ✅ | ❌ | ❌ | Handle custom request interception |
| `onConsoleMessage` | ✅ | ✅ | ❌ | ❌ | JavaScript console debugging |
| `shouldOverrideUrlLoading` | ✅ | ✅ | ✅ | ❌ | Custom URL handling |
| JS Dialogs (Alert/Confirm/Prompt) | ✅ | ✅ | ❌ | ❌ | Custom dialog UI |
| Custom View (Fullscreen) | ✅ | ⚠️ | ❌ | ❌ | Android: custom view injection, iOS: native fullscreen events |
| File Upload | ✅ | ✅ | ❌ | ❌ | Native file picker |
| Download Handling | ✅ | ⚠️ | ❌ | ❌ | `onDownloadStart` callback |

#### JSBridge

| Feature | Android | iOS | Desktop | Web | Notes |
|---------|:-------:|:---:|:-------:|:---:|-------|
| `register()` handler | ✅ | ✅ | ✅ | ✅ | Kotlin ↔ JavaScript calls |
| `emit()` events | ✅ | ✅ | ⚠️ | ⚠️ | Event bus system |
| Promise-based response | ✅ | ✅ | ⚠️ | ⚠️ | Async/await support |
| Type-safe serialization | ✅ | ✅ | ✅ | ✅ | Kotlinx Serialization |

**Legend**: ✅ Full Support | ⚠️ Partial/Limited | ❌ Not Supported

</details>

## 🎯 Project Focus & Comparison

This library has a slightly different focus compared to other WebView libraries (like `KevinnZou/compose-webview-multiplatform`), primarily targeting **Mobile productivity**.

### 1. Mobile-First & Advanced JSBridge

We focused heavily on making the interaction between Kotlin and JavaScript as seamless as possible on **Android & iOS**.

- **Promise-based JSBridge**: Enables using `await` in JavaScript to call Native functions and get results directly, avoiding callback hell.
- **Flexible Serialization**: Supports **Kotlinx Serialization** out-of-the-box, while the `BridgeSerializer` interface allows plugging in Gson, Moshi, or any other JSON library.
- **Type Safety**: Allows passing complex data objects with full type safety.

### 2. Platform Support Status

- **Mobile (Android/iOS)**: Stable and Feature-Rich. Recommended for production.

- **Desktop & Web**: Currently **Experimental (WIP)**.
  - While we have implemented basic controls and JSBridge for these platforms, they are not as battle-tested as the mobile targets.
  - For **stable Desktop or Web requirements**, [KevinnZou's library](https://github.com/KevinnZou/compose-webview-multiplatform) is the **better choice**.

---

## 📦 Installation

This library is available on **Maven Central** for universal access without authentication.

> **Current coordinates**: `io.github.parkwoocheol:compose-webview:<version>`

### Step 1. Configure Repository

Add Maven Central to `settings.gradle.kts` (or `settings.gradle`):

**Kotlin DSL (`settings.gradle.kts`):**

```kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
```

**Groovy DSL (`settings.gradle`):**

```groovy
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
```

### Step 2. Add Dependency

Add the dependency to your **commonMain** source set (for KMP) or app dependencies.

**Kotlin DSL (`build.gradle.kts`):**

```kotlin
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.parkwoocheol:compose-webview:<version>")
        }
    }
}
```

**Groovy DSL (`build.gradle`):**

```groovy
kotlin {
    sourceSets {
        commonMain {
            dependencies {
                implementation 'io.github.parkwoocheol:compose-webview:<version>'
            }
        }
    }
}
```

### Platform-Specific Artifacts

When you add the dependency:

```kotlin
implementation("io.github.parkwoocheol:compose-webview:<version>")
```

The Kotlin Multiplatform Gradle plugin **automatically selects** the correct platform-specific artifact for each target:

| Platform | Artifact ID | Maven Coordinates |
|----------|-------------|-------------------|
| **Android** | `compose-webview-android` | `io.github.parkwoocheol:compose-webview-android:<version>` |
| **iOS (arm64)** | `compose-webview-iosarm64` | `io.github.parkwoocheol:compose-webview-iosarm64:<version>` |
| **iOS (x64)** | `compose-webview-iosx64` | `io.github.parkwoocheol:compose-webview-iosx64:<version>` |
| **iOS (Simulator arm64)** | `compose-webview-iossimulatorarm64` | `io.github.parkwoocheol:compose-webview-iossimulatorarm64:<version>` |
| **Desktop (JVM)** | `compose-webview-desktop` | `io.github.parkwoocheol:compose-webview-desktop:<version>` |
| **Web (JS)** | `compose-webview-js` | `io.github.parkwoocheol:compose-webview-js:<version>` |
| **Web (WASM)** | `compose-webview-wasmjs` | `io.github.parkwoocheol:compose-webview-wasmjs:<version>` |
| **Metadata** | `compose-webview` | `io.github.parkwoocheol:compose-webview:<version>` |

> **Note**: You don't need to specify platform-specific artifacts manually. Just use `compose-webview` and Gradle resolves the correct artifact automatically.

## Quick Start

```kotlin
@Composable
fun MyWebViewScreen() {
    ComposeWebView(
        url = "https://example.com",
        modifier = Modifier.fillMaxSize(),
        onCreated = { webView ->
            webView.settings.javaScriptEnabled = true
        }
    )
}
```

## State Management

The library provides two ways to create and remember the `WebViewState`:

### 1. Persistent State (Recommended)

Uses `rememberSaveable` to preserve the latest top-level WebView request across configuration changes (e.g., screen rotation). On Android, native `WebView` state is also restored when available. Other platforms restore the latest top-level `loadUrl()` or `loadHtml()` request rather than the full browser session.

```kotlin
val state = rememberSaveableWebViewState(url = "https://google.com")
// or for HTML data
val state = rememberSaveableWebViewStateWithData(data = htmlContent)
```

`WebViewController.loadUrl()` and `loadHtml()` participate in this restoration flow. `postUrl()` still runs on Android/iOS, but it is intentionally not auto-replayed after recreation because resubmitting POST requests can cause side effects.

### 2. Transient State (Lightweight)

Uses `remember` to hold the state. The WebView will be reloaded and state lost on configuration changes. Use this if you want to avoid `TransactionTooLargeException` with large HTML data or don't need state persistence.

```kotlin
val state = rememberWebViewState(url = "https://google.com")
// or for HTML data
val state = rememberWebViewStateWithData(data = htmlContent)
```

## Usage

### Basic Usage

Create a simple WebView with minimal configuration:

```kotlin
@Composable
fun SimpleWebView() {
    ComposeWebView(
        url = "https://google.com",
        modifier = Modifier.fillMaxSize(),
        onCreated = { webView ->
            webView.settings.apply {
                javaScriptEnabled = true
                domStorageEnabled = true
                loadWithOverviewMode = true
                useWideViewPort = true
            }
        }
    )
}
```

### WebView State and Controller

Monitor and control WebView state using `WebViewController`:

```kotlin
@Composable
fun WebViewWithState() {
    val state = rememberSaveableWebViewState(url = "https://example.com")
    val controller = rememberWebViewController()

    Column(modifier = Modifier.fillMaxSize()) {
        // Show loading indicator
        val loadingState = state.loadingState
        if (loadingState is LoadingState.Loading) {
            LinearProgressIndicator(
                progress = { loadingState.progress },
                modifier = Modifier.fillMaxWidth()
            )
        }

        // Display current URL
        Text(text = "Current URL: ${state.lastLoadedUrl}")

        // Navigation controls
        Row {
            Button(
                onClick = { controller.navigateBack() },
                enabled = controller.canGoBack
            ) {
                Text("Back")
            }
            Button(
                onClick = { controller.navigateForward() },
                enabled = controller.canGoForward
            ) {
                Text("Forward")
            }
            Button(onClick = { controller.reload() }) {
                Text("Reload")
            }
        }

        ComposeWebView(
            state = state,
            controller = controller,
            modifier = Modifier
                .fillMaxWidth()
                .weight(1f),
            onCreated = { it.settings.javaScriptEnabled = true }
        )
    }
}
```

### JSBridge - Kotlin to JavaScript Communication

Communicate between Kotlin and JavaScript using type-safe, promise-based API.

#### Define Data Models

```kotlin
@Serializable
data class User(val name: String, val age: Int)

@Serializable
data class UserResponse(val success: Boolean, val message: String)

@Serializable
data class Location(val latitude: Double, val longitude: Double)
```

#### Register Handlers in Kotlin

```kotlin
@Composable
fun WebViewWithJsBridge() {
    val state = rememberSaveableWebViewState(url = "https://example.com")
    val bridge = rememberWebViewJsBridge()

    LaunchedEffect(bridge) {
        // Handle "updateUser" call from JavaScript
        bridge.register<User, UserResponse>("updateUser") { user ->
            println("Updating user: ${user.name}, age: ${user.age}")
            // Perform some operation
            UserResponse(success = true, message = "User updated successfully")
        }

        // Handle "getLocation" call
        bridge.register<Unit, Location>("getLocation") { _ ->
            // Return current location
            Location(latitude = 37.7749, longitude = -122.4194)
        }

        // Handle void calls (no return value)
        bridge.register<String, Unit>("log") { message ->
            println("JavaScript log: $message")
        }

        // Handle nullable input payloads
        bridge.registerNullable<User, UserResponse>("updateUserMaybe") { userOrNull ->
            if (userOrNull == null) {
                UserResponse(success = false, message = "Missing user payload")
            } else {
                UserResponse(success = true, message = "Updated ${userOrNull.name}")
            }
        }

        // Suspend handlers — async operations before returning to JS
        bridge.register<SearchQuery, SearchResult>("search") { query ->
            val results = api.search(query.term) // suspend call
            SearchResult(items = results)
        }

        bridge.registerWithContext<User, UserResponse>("updateUserFromTrustedFrame") { user ->
            println("Called from origin=$sourceOrigin mainFrame=$isMainFrame via $transport")
            UserResponse(success = true, message = "Handled ${user.name}")
        }
    }

    ComposeWebView(
        state = state,
        jsBridge = bridge,
        modifier = Modifier.fillMaxSize()
    )
}
```

#### Call from JavaScript

```javascript
// The default bridge object is 'window.AppBridge'

// Call with typed response
async function updateUser() {
    try {
        const response = await window.AppBridge.call('updateUser', {
            name: "Park",
            age: 30
        });
        console.log("Success:", response.message);
    } catch (error) {
        console.error("Failed:", error);
    }
}

// Call without parameters
async function getLocation() {
    try {
        // You can also omit the second argument:
        // const location = await window.AppBridge.call('getLocation');
        const location = await window.AppBridge.call('getLocation', null);
        console.log(`Lat: ${location.latitude}, Lng: ${location.longitude}`);
    } catch (error) {
        console.error("Failed to get location:", error);
    }
}

// Call without return value
async function logMessage() {
    try {
        await window.AppBridge.call('log', "Hello from JavaScript!");
    } catch (error) {
        console.error("Failed to log:", error);
    }
}
```

`register<T, R>` null input rules:
- `T` is `Unit`: `call("method")` and `call("method", null)` are both accepted.
- Other non-null input types: `null` throws an input type error.

Use `registerNullable<T, R>` when a payload type may be `null` from JavaScript.

`registerWithContext<T, R>` exposes `sourceOrigin`, `isMainFrame`, and `transport`. On Android these values are
most useful with `rememberAndroidWebViewJsBridge(...)`, which uses origin-aware WebView message APIs for documents
matched by `allowedOriginRules`.

#### Choosing a Bridge

| Use case | API | Notes |
|----------|-----|-------|
| Cross-platform typed calls and events | `rememberWebViewJsBridge()` | Default for shared code. On Android it keeps the compatibility bridge path. |
| Android typed calls with origin and frame metadata | `rememberAndroidWebViewJsBridge(..., policy = OriginAwareOnly)` | Android-only. Uses `addWebMessageListener` with `allowedOriginRules`. |
| Native-to-JavaScript main-frame messages on Android | `postMainFrameMessage(...)` | Android-only experimental API. Maps to `postWebMessage`. |
| Long-lived bidirectional main-frame channel on Android | `openMainFrameSession(...)` | Android-only experimental API. Maps to `WebMessageChannel` plus `postWebMessage`. |
| Provider or page fallback that cannot use the origin-aware path | `policy = Compatible` | Re-enables the classic Android bridge fallback. Use only when you accept the weaker trust model. |

#### Android Origin-Aware Bridge

For Android web content served from origins listed in `allowedOriginRules`, prefer
`rememberAndroidWebViewJsBridge(...)` from your Android source set over the default bridge:

```kotlin
val bridge =
    rememberAndroidWebViewJsBridge(
        config =
            AndroidWebViewJsBridgeConfig(
                allowedOriginRules = setOf("https://example.com"),
                policy = AndroidJsBridgePolicy.OriginAwareOnly,
            ),
    )
```

Notes:
- `rememberWebViewJsBridge()` remains the default cross-platform bridge.
- On Android, `rememberWebViewJsBridge()` is compatibility-oriented and keeps the existing `addJavascriptInterface`
  flow.
- `rememberAndroidWebViewJsBridge()` enables `addWebMessageListener` + document-start bootstrap with
  `allowedOriginRules`, and can fall back to a compatibility bridge when `policy = Compatible`.
- `policy = Compatible` is a fallback mode. It intentionally restores `addJavascriptInterface`-style
  exposure for pages that do not receive the origin-aware bridge, so it should not be treated as equivalent to
  `OriginAwareOnly`.

#### Android Experimental Message APIs

The origin-aware Android bridge also exposes optional experimental APIs for raw string/binary messages and main-frame
message-channel sessions:

```kotlin
@OptIn(ExperimentalComposeWebViewApi::class)
LaunchedEffect(bridge) {
    bridge.registerMessage("echoText") { message ->
        when (message) {
            is AndroidBridgeMessage.Text -> AndroidBridgeMessage.Text("echo:${message.value}")
            is AndroidBridgeMessage.Binary -> AndroidBridgeMessage.Binary(message.value)
        }
    }

    bridge.postMainFrameMessage(
        targetOrigin = "https://example.com",
        message = AndroidBridgeMessage.Text("native-ready"),
    )

    val session = bridge.openMainFrameSession("https://example.com")
    session?.setMessageHandler { payload ->
        println("Session payload: $payload")
    }
}
```

These experimental APIs map directly to Android `postWebMessage` and `WebMessageChannel`. Treat them as
main-frame transport primitives, not as the same trust boundary as inbound `addWebMessageListener`. Use an exact
`targetOrigin` such as `https://example.com`; wildcard target origins are rejected.

JavaScript helpers added by the origin-aware Android bridge:

```javascript
window.AppBridge.onMessage((payload) => {
  console.log('Native message:', payload);
});

window.AppBridge.onSession((session) => {
  session.onMessage((payload) => console.log('Session payload:', payload));
  session.postMessage('hello from JS session');
});

const echoed = await window.AppBridge.callMessage('echoText', 'hello');
```

#### Customizing Bridge Name

Change the global JavaScript object name:

```kotlin
val bridge = rememberWebViewJsBridge(
    jsObjectName = "MyBridge" // Access via window.MyBridge in JavaScript
)
```

### Lifecycle Management

ComposeWebView automatically handles lifecycle events:

```kotlin
@Composable
fun WebViewWithLifecycle() {
    val state = rememberSaveableWebViewState(url = "https://example.com")

    // Lifecycle events are handled automatically:
    // - onResume() when composable enters composition
    // - onPause() when composable leaves composition
    // - Cleanup when disposed

    ComposeWebView(
        state = state,
        modifier = Modifier.fillMaxSize()
    )
}
```

### Error Handling

Handle WebView errors and display custom error UI:

```kotlin
@Composable
fun WebViewWithErrorHandling() {
    val state = rememberSaveableWebViewState(url = "https://example.com")
    val controller = rememberWebViewController()

    // Configure client to handle errors
    val client = rememberWebViewClient {
        onReceivedError { view, request, error ->
            // Handle error
            Log.e("WebView", "Error loading ${request?.url}: ${error?.description}")
        }
    }

    ComposeWebView(
        state = state,
        controller = controller,
        client = client,
        modifier = Modifier.fillMaxSize(),
        errorContent = { errors ->
            // Custom error UI
            Box(
                modifier = Modifier.fillMaxSize(),
                contentAlignment = Alignment.Center
            ) {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    Text(
                        text = "Failed to load page",
                        style = MaterialTheme.typography.headlineMedium
                    )
                    errors.firstOrNull()?.let { error ->
                        Text(
                            text = error.description,
                            style = MaterialTheme.typography.bodyMedium
                        )
                    }
                    Button(onClick = { controller.reload() }) {
                        Text("Retry")
                    }
                }
            }
        }
    )
}
```

### Custom WebView Configuration

Configure WebView behavior using `WebViewSettings`:

```kotlin
@Composable
fun CustomWebView() {
    val state = rememberSaveableWebViewState(url = "https://example.com")

    val settings = WebViewSettings(
        userAgent = "MyApp/1.0",
        javaScriptEnabled = true,
        domStorageEnabled = true,
        cacheMode = CacheMode.CACHE_ELSE_NETWORK,
        supportZoom = true,
        loadWithOverviewMode = true,
        useWideViewPort = true,
        mediaPlaybackRequiresUserAction = false,
        allowFileAccess = false,
        allowContentAccess = false,
        allowFileAccessFromFileURLs = false,
        allowUniversalAccessFromFileURLs = false
    )

    ComposeWebView(
        state = state,
        settings = settings,
        modifier = Modifier.fillMaxSize(),
        onCreated = { webView ->
            // Additional platform-specific configuration
        },
        onDispose = { webView ->
            // Custom cleanup logic
        }
    )
}
```

### Scroll Position Tracking

Track and respond to scroll position changes:

```kotlin
@Composable
fun ScrollTrackingWebView() {
    val state = rememberSaveableWebViewState(url = "https://example.com")
    val controller = rememberWebViewController()
    var showScrollToTop by remember { mutableStateOf(false) }

    // Observe scroll position changes
    LaunchedEffect(state.scrollPosition) {
        val (x, y) = state.scrollPosition
        showScrollToTop = y > 500 // Show button when scrolled down 500px
    }

    Box(modifier = Modifier.fillMaxSize()) {
        ComposeWebView(
            state = state,
            controller = controller,
            modifier = Modifier.fillMaxSize()
        )

        // Floating "Back to Top" button
        if (showScrollToTop) {
            FloatingActionButton(
                onClick = {
                    controller.scrollTo(0, 0)
                },
                modifier = Modifier
                    .align(Alignment.BottomEnd)
                    .padding(16.dp)
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to top")
            }
        }
    }
}
```

### Console Message Debugging

Capture JavaScript console messages for debugging:

```kotlin
@Composable
fun DebuggableWebView() {
    val state = rememberSaveableWebViewState(url = "https://example.com")

    // Configure chrome client to handle console messages
    val chromeClient = rememberWebChromeClient {
        onConsoleMessage { webView, message ->
            when (message.level) {
                ConsoleMessageLevel.ERROR -> {
                    Log.e("WebView", "[${message.sourceId}:${message.lineNumber}] ${message.message}")
                }
                ConsoleMessageLevel.WARNING -> {
                    Log.w("WebView", message.message)
                }
                ConsoleMessageLevel.LOG -> {
                    Log.d("WebView", message.message)
                }
                else -> {
                    Log.v("WebView", message.message)
                }
            }
            false // Return true to suppress default logging
        }
    }

    ComposeWebView(
        state = state,
        chromeClient = chromeClient,
        modifier = Modifier.fillMaxSize()
    )
}
```

### Configuring WebView Clients

Configure WebView client events using `rememberWebViewClient` and `rememberWebChromeClient`:

#### Basic Usage

```kotlin
@Composable
fun WebViewWithClientConfiguration() {
    val state = rememberSaveableWebViewState(url = "https://example.com")

    // Configure WebViewClient
    val client = rememberWebViewClient {
        onPageStarted { view, url, favicon ->
            println("Page started loading: $url")
        }
        onPageFinished { view, url ->
            println("Page finished loading: $url")
        }
        onReceivedError { view, request, error ->
            println("Error loading page: ${error?.description}")
        }
        shouldOverrideUrlLoading { view, request ->
            // Return true to block navigation
            request?.url?.startsWith("myapp://") == true
        }
    }

    // Configure WebChromeClient
    val chromeClient = rememberWebChromeClient {
        onProgressChanged { view, progress ->
            println("Loading progress: $progress%")
        }
        onConsoleMessage { view, message ->
            println("[Console ${message.level}] ${message.message}")
            false // Return true to suppress default handling
        }
        onPermissionRequest { request ->
            // Handle permission requests (platform-specific)
        }
    }

    ComposeWebView(
        state = state,
        client = client,
        chromeClient = chromeClient,
        modifier = Modifier.fillMaxSize()
    )
}
```

#### Chaining Multiple Handlers

You can also chain handlers:

```kotlin
@Composable
fun ChainedHandlersExample() {
    val client = rememberWebViewClient()
        .onPageStarted { view, url, favicon -> /* ... */ }
        .onPageFinished { view, url -> /* ... */ }
        .onReceivedError { view, request, error -> /* ... */ }

    val chromeClient = rememberWebChromeClient()
        .onProgressChanged { view, progress -> /* ... */ }
        .onConsoleMessage { view, message -> false }

    ComposeWebView(
        state = rememberSaveableWebViewState(url = "https://example.com"),
        client = client,
        chromeClient = chromeClient,
        modifier = Modifier.fillMaxSize()
    )
}
```

#### Advanced Customization

For more advanced scenarios, you can still extend the client classes directly:

```kotlin
@Composable
fun AdvancedClientExample() {
    val client = remember {
        object : ComposeWebViewClient() {
            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                // Full control with direct override
                view?.evaluateJavascript("console.log('Custom injection')")
            }
        }
    }

    ComposeWebView(
        state = rememberSaveableWebViewState(url = "https://example.com"),
        client = client,
        modifier = Modifier.fillMaxSize()
    )
}
```

#### Available Client Handlers

**WebViewClient:**

- `onPageStarted(handler: (WebView?, String?, PlatformBitmap?) -> Unit)`
- `onPageFinished(handler: (WebView?, String?) -> Unit)`
- `onReceivedError(handler: (WebView?, PlatformWebResourceRequest?, PlatformWebResourceError?) -> Unit)`
- `shouldInterceptRequest(handler: (WebView?, PlatformWebResourceRequest?) -> PlatformWebResourceResponse?)`
- `shouldOverrideUrlLoading(handler: (WebView?, PlatformWebResourceRequest?) -> Boolean)`

**WebChromeClient:**

- `onProgressChanged(handler: (WebView?, Int) -> Unit)`
- `onConsoleMessage(handler: (WebView?, ConsoleMessage) -> Boolean)`
- `onPermissionRequest(handler: (PlatformPermissionRequest) -> Unit)` (Platform-specific)

## API Reference

### ComposeWebView

Main composable function for displaying WebView.

```kotlin
@Composable
fun ComposeWebView(
    url: String,
    modifier: Modifier = Modifier,
    settings: WebViewSettings = WebViewSettings.Default,
    releaseStrategy: WebViewReleaseStrategy = WebViewReleaseStrategy.DestroyOnRelease,
    controller: WebViewController = rememberWebViewController(),
    javaScriptInterfaces: Map<String, Any> = emptyMap(),
    onCreated: (WebView) -> Unit = {},
    onDispose: (WebView) -> Unit = {},
    client: ComposeWebViewClient = remember { ComposeWebViewClient() },
    chromeClient: ComposeWebChromeClient = remember { ComposeWebChromeClient() },
    factory: ((PlatformContext) -> WebView)? = null,
    loadingContent: @Composable () -> Unit = {},
    errorContent: @Composable (List<WebViewError>) -> Unit = {},
    jsAlertContent: @Composable (JsDialogState.Alert) -> Unit = {},
    jsConfirmContent: @Composable (JsDialogState.Confirm) -> Unit = {},
    jsPromptContent: @Composable (JsDialogState.Prompt) -> Unit = {},
    customViewContent: (@Composable (CustomViewState) -> Unit)? = null,
    onDownloadStart: ((String, String, String, String, Long) -> Unit)? = null,
    onFindResultReceived: ((Int, Int, Boolean) -> Unit)? = null,
    onStartActionMode: ((WebView, PlatformActionModeCallback?) -> PlatformActionModeCallback?)? = null,
)

// Alternative overload with state
@Composable
fun ComposeWebView(
    state: WebViewState,
    modifier: Modifier = Modifier,
    settings: WebViewSettings = WebViewSettings.Default,
    releaseStrategy: WebViewReleaseStrategy = WebViewReleaseStrategy.DestroyOnRelease,
    controller: WebViewController = rememberWebViewController(),
    javaScriptInterfaces: Map<String, Any> = emptyMap(),
    jsBridge: WebViewJsBridge? = null,
    // ... other parameters same as above
)
```

### WebViewState

Holds the state of the WebView.

```kotlin
class WebViewState(
    webContent: WebContent
) {
    var lastLoadedUrl: String?
    var content: WebContent
    val loadingState: LoadingState
    val isLoading: Boolean
    var pageTitle: String?
    var pageIcon: PlatformBitmap?
    var scrollPosition: ScrollPosition
    val errorsForCurrentRequest: SnapshotStateList<WebViewError>
    var jsDialogState: JsDialogState?
    var customViewState: CustomViewState?
    var webView: WebView?
    var bundle: PlatformBundle?
}
```

### WebViewController

Controls WebView navigation and operations.

```kotlin
class WebViewController {
    val canGoBack: Boolean
    val canGoForward: Boolean

    fun loadUrl(url: String, additionalHttpHeaders: Map<String, String> = emptyMap())
    fun loadHtml(
        html: String,
        baseUrl: String? = null,
        mimeType: String? = null,
        encoding: String? = "UTF-8",
        historyUrl: String? = null
    )
    fun postUrl(url: String, postData: ByteArray)
    fun evaluateJavascript(script: String, callback: ((String) -> Unit)? = null)
    fun navigateBack()
    fun navigateForward()
    fun reload()
    fun stopLoading()
    fun zoomBy(zoomFactor: Float)
    fun zoomIn()
    fun zoomOut()
    fun findAllAsync(find: String)
    fun findNext(forward: Boolean)
    fun clearMatches()
    fun clearCache()
    fun clearHistory()
    fun clearSslPreferences()
    fun clearFormData()
    fun pageUp(top: Boolean)
    fun pageDown(bottom: Boolean)
    fun scrollTo(x: Int, y: Int)
    fun scrollBy(x: Int, y: Int)
    fun saveWebArchive(filename: String)
}
```

### WebViewJsBridge

Manages JavaScript-Kotlin communication.

```kotlin
class WebViewJsBridge(
    serializer: BridgeSerializer? = null,
    val jsObjectName: String = "AppBridge",
    private val nativeInterfaceName: String = "AppBridgeNative"
) {
    val capabilities: BridgeCapabilities

    // Register a typed handler for calls from JavaScript.
    // Accepts both regular and suspend lambdas.
    // Null input is only accepted when T is Unit.
    inline fun <reified T : Any, reified R : Any> register(
        method: String,
        noinline handler: suspend (T) -> R
    )

    // Register a typed handler with invocation metadata.
    inline fun <reified T : Any, reified R : Any> registerWithContext(
        method: String,
        noinline handler: suspend BridgeInvocationContext.(T) -> R
    )

    // Register a no-argument handler
    inline fun <reified R : Any> register(
        method: String,
        noinline handler: suspend () -> R
    )

    // Register a handler for nullable payload input
    inline fun <reified T : Any, reified R : Any> registerNullable(
        method: String,
        noinline handler: suspend (T?) -> R
    )

    // Emit an event to JavaScript
    inline fun <reified T> emit(eventName: String, data: T)

    // Unregister a handler
    fun unregister(method: String)
}
```

### ComposeWebViewClient

Handles navigation and page lifecycle events. Can be configured using extension functions.

```kotlin
expect open class ComposeWebViewClient() {
    open var webViewState: WebViewState?
    open var webViewController: WebViewController?

    open fun onPageStarted(view: WebView?, url: String?, favicon: PlatformBitmap?)
    open fun onPageFinished(view: WebView?, url: String?)
    open fun onReceivedError(view: WebView?, request: PlatformWebResourceRequest?, error: PlatformWebResourceError?)
    open fun shouldOverrideUrlLoading(view: WebView?, request: PlatformWebResourceRequest?): Boolean
}
```

**Extension Functions for Configuration:**

```kotlin
// Configure callbacks using chainable extension functions
fun ComposeWebViewClient.onPageStarted(
    handler: (WebView?, String?, PlatformBitmap?) -> Unit
): ComposeWebViewClient

fun ComposeWebViewClient.onPageFinished(
    handler: (WebView?, String?) -> Unit
): ComposeWebViewClient

fun ComposeWebViewClient.onReceivedError(
    handler: (WebView?, PlatformWebResourceRequest?, PlatformWebResourceError?) -> Unit
): ComposeWebViewClient

fun ComposeWebViewClient.shouldOverrideUrlLoading(
    handler: (WebView?, PlatformWebResourceRequest?) -> Boolean
): ComposeWebViewClient
```

### ComposeWebChromeClient

Handles UI events like progress updates and console messages. Can be configured using extension functions.

```kotlin
expect open class ComposeWebChromeClient() {
    open fun onProgressChanged(view: WebView?, newProgress: Int)
    open fun onConsoleMessage(view: WebView?, message: ConsoleMessage): Boolean
}
```

**Extension Functions for Configuration:**

```kotlin
// Configure callbacks using chainable extension functions
fun ComposeWebChromeClient.onProgressChanged(
    handler: (WebView?, Int) -> Unit
): ComposeWebChromeClient

fun ComposeWebChromeClient.onConsoleMessage(
    handler: (WebView?, ConsoleMessage) -> Boolean
): ComposeWebChromeClient

fun ComposeWebChromeClient.onPermissionRequest(
    handler: (PlatformPermissionRequest) -> Unit
): ComposeWebChromeClient
```

### Remember Functions

```kotlin
@Composable
fun rememberWebViewState(
    url: String,
    additionalHttpHeaders: Map<String, String> = emptyMap()
): WebViewState

@Composable
fun rememberSaveableWebViewState(
    url: String,
    additionalHttpHeaders: Map<String, String> = emptyMap()
): WebViewState

@Composable
fun rememberWebViewStateWithData(
    data: String,
    baseUrl: String? = null,
    encoding: String = "utf-8",
    mimeType: String = "text/html",
    historyUrl: String? = null
): WebViewState

@Composable
fun rememberSaveableWebViewStateWithData(
    data: String,
    baseUrl: String? = null,
    encoding: String = "utf-8",
    mimeType: String = "text/html",
    historyUrl: String? = null
): WebViewState

@Composable
fun rememberWebViewController(): WebViewController

@Composable
fun rememberWebViewClient(
    block: (ComposeWebViewClient.() -> Unit)? = null
): ComposeWebViewClient

@Composable
fun rememberWebChromeClient(
    block: (ComposeWebChromeClient.() -> Unit)? = null
): ComposeWebChromeClient

@Composable
fun rememberWebViewJsBridge(
    serializer: BridgeSerializer? = null,
    jsObjectName: String = "AppBridge",
    nativeInterfaceName: String = "AppBridgeNative"
): WebViewJsBridge

@Composable
fun rememberAndroidWebViewJsBridge(
    serializer: BridgeSerializer? = null,
    jsObjectName: String = "AppBridge",
    nativeInterfaceName: String = "AppBridgeNative",
    config: AndroidWebViewJsBridgeConfig
): WebViewJsBridge
```

### Custom Serializer

Implement `BridgeSerializer` to use custom JSON libraries like Gson or Moshi:

```kotlin
import kotlin.reflect.KType

interface BridgeSerializer {
    fun encode(data: Any?, type: KType): String
    fun <T> decode(json: String, type: KType): T
}

// Example skeleton for a custom serializer
class CustomBridgeSerializer : BridgeSerializer {
    override fun encode(data: Any?, type: KType): String {
        TODO("Serialize data with your JSON library using type")
    }

    override fun <T> decode(json: String, type: KType): T {
        TODO("Deserialize json with your JSON library using type")
    }
}

// Usage
val bridge = rememberWebViewJsBridge(
    serializer = CustomBridgeSerializer()
)
```

## Sample Apps

The `sample/` directory contains runnable targets for every platform:

- `sample/shared`: the Compose Multiplatform UI/features showcased below.
- `sample/androidApp`: Android wrapper (Gradle module).
- `sample/iosApp`: Xcode project that embeds the shared UI.
- `sample/desktopApp`: Compose Desktop entry point.
- `sample/wasmApp`: Compose WASM/browser runner.

### Features Demonstrated

All sample targets use the same feature screens:

1. **Basic Browser** (`BasicBrowserScreen`)
    - Standard WebView with navigation controls (Back, Forward, Reload).
    - URL input field with loading progress indicator.

2. **Transient vs Saved State** (`TransientBrowserScreen`)
    - Demonstrates the difference between `rememberWebViewState` (transient) and `rememberSaveableWebViewState` (persisted).
    - Rotate the device to see the transient state reset while the saved state persists.

3. **HTML & JS Interaction** (`HtmlJsScreen`)
    - **Bi-directional Communication**: Send data from Kotlin to JS and vice-versa.
    - **Command Center**: Trigger Native events from UI and see JS logs in real-time.
    - **Promise-based API**: Call Native functions from JS using `await`.

4. **Fullscreen Video** (`FullscreenVideoScreen`)
    - Native fullscreen video support (e.g., YouTube).
    - Handles orientation changes and UI overlay automatically.
    - iOS uses the system fullscreen player; the Compose state lets you hide your chrome when it appears.

5. **Custom Client** (`CustomClientScreen`)
    - Configure WebView settings (JS, DOM Storage, Zoom) dynamically.
    - Inject custom `WebViewClient` to intercept URLs (e.g., blocking specific domains).

### Running the Samples

- **Android**: `./gradlew :sample:androidApp:installDebug`
- **Desktop**: `./gradlew :sample:desktopApp:run`
- **Web/Wasm**: `./gradlew :sample:wasmApp:wasmJsBrowserDevelopmentRun` (open the printed URL in a browser).
  - **Note**: WASM target is enabled by default. If you encounter build issues, you can disable it by adding `-PENABLE_WASM=false` to any Gradle command.
- **iOS**: Open `sample/iosApp/iosApp.xcodeproj` in Xcode and run the `iosApp` scheme.

## Contributing

We welcome contributions from the community! Whether it's reporting bugs, suggesting features, improving docs, or submitting code.

### Contributing Quick Start

1. Fork the repository
2. Create your branch (`git checkout -b your-branch-name`)
3. Make your changes
4. Test with the sample app
5. Push and open a Pull Request

See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.

### Release History

Release history is maintained in [CHANGELOG.md](CHANGELOG.md).

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
