diff --git a/docs/components/overview.md b/docs/components/overview.md
index bc1be3e..4faebdd 100644
--- a/docs/components/overview.md
+++ b/docs/components/overview.md
@@ -17,7 +17,8 @@ regular browser.
## `useBridgeComponent`
-The React entry exposes one generic primitive:
+Each framework entry exposes one generic primitive — the examples below use
+React; Vue and Svelte have the same shape (see the [API](/reference/api#usebridgecomponent)):
```js
import { useBridgeComponent } from 'inertia-native/react'
@@ -64,6 +65,26 @@ useEffect(() => {
Without the cleanup, a changed prop leaves the old callback registered and every
subsequent interaction is reported twice.
+## Restoring after a native screen
+
+On Android, coming back to a web screen from a native one dispatches
+`native:restore`, and native may have dropped the UI a component drew on
+`connect`. `useBridgeComponent` exposes this as `restored`, a counter. Put it
+in the deps of the effect that sends `connect`, so it is sent again:
+
+```js
+const { supported, send, restored } = useBridgeComponent('button')
+
+useEffect(() => {
+ if (!supported) return
+ const id = send('connect', { title }, () => onTapRef.current?.())
+ return () => window.HotwireNative?.web?.removeCallback(id)
+}, [supported, title, send, restored])
+```
+
+Nothing is resent automatically: a component that only sends when called (like
+`alert`) must not fire again.
+
## Ready-made components
Rather than writing each component from scratch, you can copy one from the
diff --git a/docs/guide/installation.md b/docs/guide/installation.md
index 9df81b4..a793557 100644
--- a/docs/guide/installation.md
+++ b/docs/guide/installation.md
@@ -1,37 +1,43 @@
# Installation
-::: warning Beta
-Published under the `beta` tag while the API stabilizes. Install with `@beta`.
-:::
-
```bash
-npm add inertia-native@beta
+npm add inertia-native
```
## Requirements
-- `@inertiajs/core` >= 2.0 (works with the v3 line)
+- `@inertiajs/core` >= 3.0
- `react` >= 18 — only for the `inertia-native/react` entry
+- `vue` >= 3.0 — only for the `inertia-native/vue` entry
+- `svelte` >= 4.0 — only for the `inertia-native/svelte` entry
## Setup
-Call `initHotwireNative()` once, **before** `createInertiaApp`, in your Inertia
+Call `initInertiaNative()` once, **before** `createInertiaApp`, in your Inertia
entrypoint:
```js
import { createInertiaApp } from '@inertiajs/react'
-import { initHotwireNative } from 'inertia-native'
+import { initInertiaNative } from 'inertia-native'
-const isHotwireNative = !!window.webkit?.messageHandlers?.turbo
-initHotwireNative({ debug: import.meta.env.DEV || isHotwireNative })
+const isNativeApp = !!window.webkit?.messageHandlers?.turbo
+initInertiaNative({ debug: import.meta.env.DEV || isNativeApp })
createInertiaApp({ /* ... */ })
```
That's all that's needed for native navigation (push/pop/replace/restore,
-modals, forms, error screens, pull-to-refresh).
+modals, forms, error screens, pull-to-refresh). See the [API](/reference/api)
+for the available options.
To set up the native iOS and Android shells that load your app, see
[Native apps → iOS](/native/ios) and [Android](/native/android).
+## Upgrading from 0.1.0-beta
+
+- `initHotwireNative` is now `initInertiaNative`.
+- `@inertiajs/core` 2.x is no longer supported.
+- The package was published as `inertia-hotwire-native` before
+ `0.1.0-beta.2`; replace that dependency with `inertia-native`.
+
Next: [Navigation](/guide/navigation).
diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md
index 46de47c..1ce16fd 100644
--- a/docs/guide/introduction.md
+++ b/docs/guide/introduction.md
@@ -5,10 +5,9 @@ lets an [Inertia.js](https://inertiajs.com) application run inside
[Hotwire Native](https://native.hotwired.dev) (iOS & Android) and behave like a
native app.
-::: warning Beta & unofficial
-This is a pre-1.0, community-built integration. It is **not** an official
-Inertia.js or Hotwire project, and the API may still change between beta
-releases.
+::: info Unofficial
+This is a community-built integration. It is **not** an official Inertia.js or
+Hotwire project.
:::
## How it works
@@ -22,12 +21,15 @@ bridge components (submit buttons, menus, etc.).
In a regular browser it stays inert: with no native adapter connected, Inertia
navigates exactly as usual.
-## Two entry points
+## Entry points
- **Framework-agnostic core** (`inertia-native`) — peer-depends on
`@inertiajs/core`. Handles all navigation.
-- **React bindings** (`inertia-native/react`) — optional, peer-depends
- on `react`. Provides `useBridgeComponent` for bridge components.
+- **Bridge component bindings** — optional, each peer-depends on its framework
+ and provides `useBridgeComponent`:
+ - `inertia-native/react`
+ - `inertia-native/vue`
+ - `inertia-native/svelte`
## The ecosystem
diff --git a/docs/guide/navigation.md b/docs/guide/navigation.md
index bc9cf9a..30f5c5a 100644
--- a/docs/guide/navigation.md
+++ b/docs/guide/navigation.md
@@ -1,6 +1,6 @@
# Navigation
-Once [`initHotwireNative()`](/reference/api#inithotwirenative) is called, every
+Once [`initInertiaNative()`](/reference/api#initinertianative) is called, every
Inertia navigation is mapped onto the native navigation stack. You don't change
how you write Inertia links or visits — the shim translates them.
@@ -11,24 +11,94 @@ how you write Inertia links or visits — the shim translates them.
| Link tap (`` / `router.visit`) | Cancelled in the web view; the native side presents the destination (push / modal). |
| `replace: true` visits | Replaces the current native screen instead of pushing. |
| Back / restore | Restores the cached page from Inertia's history (no re-fetch); falls back to a fresh request if there's no cached entry. |
-| Form submissions (non-GET) | Stay in the web view; the native side is notified via form-submission events. |
+| Form submissions (non-GET) | Stay in the web view; the native side is notified via form-submission events. See [Form redirects](#form-redirects). |
| HTTP errors (404 / 500) & network failures | Surface the native error screen for the failed visit. |
+| A non-Inertia page (e.g. a classic Turbo page) | Native reloads the screen, so the page loads as a full document. |
+| Asset version mismatch (409) | Native reloads the screen. |
+| `inertia_location` / `Inertia::location` | Handed to native, which decides where it opens — Safari, an in-app browser, a modal. |
| Pull-to-refresh | Treated as an in-place reload of the current URL. |
+## Visits that stay on the page
+
+Some visits update the current page rather than open a new one. These run in
+the web view as usual and never reach native:
+
+- partial reloads (`only`, `except`, `reset`) — including `router.reload`,
+ `usePoll`, ``, `` and deferred props
+- `async` visits, and visits with `preserveState: true` or `preserveUrl`
+- visits to the current URL
+
+Prefetches (``, `router.prefetch`) are cancelled: the next
+screen may load in another web view, so the prefetched response would be
+wasted. Hotwire Native's own `turbo.js` disables prefetching for the same
+reason.
+
## Modals
A visit presented as a modal is driven by your native path configuration
(Hotwire Native's `path-configuration` JSON), not by the package — the package
just proposes the visit and reports its lifecycle. See the native setup guides.
+## Form redirects
+
+By default a form's result stays in the web view the form was in, so a form in
+a modal shows its result inside the modal. Pass `proposeFormRedirects: true` to
+propose the page a form lands on to native instead, as Turbo does — your path
+configuration then decides, e.g. dismissing the modal to show the result in the
+main stack:
+
+```js
+initInertiaNative({ proposeFormRedirects: true })
+```
+
+A form that lands back on its own URL — validation errors — is not proposed.
+
+## Historical locations
+
+To dismiss a modal or pop a screen after a form, answer the form with one of
+Hotwire Native's historical locations: `/recede_historical_location` (pop),
+`/refresh_historical_location` (refresh) or `/resume_historical_location`
+(stay). Native handles these paths itself; no path configuration is needed.
+
+A plain redirect does not work with Inertia: Inertia follows it inside its own
+request and native never sees it. Answer with an Inertia location visit
+instead, which this package hands to native.
+
+With [turbo-rails](https://github.com/hotwired/turbo-rails), its
+`recede_or_redirect_to`, `refresh_or_redirect_to`, `resume_or_redirect_to` and
+`*_or_redirect_back_or_to` helpers all go through one private method. Override
+it in your `ApplicationController` to answer Inertia requests from the native
+app with a location visit:
+
+```ruby
+private
+
+def turbo_native_action_or_redirect(url, action, redirect_type, options = {})
+ if turbo_native_app? && request.inertia?
+ native_params = options.delete(:native_params) || {}
+ inertia_location send("turbo_#{action}_historical_location_url", notice: options[:notice], **native_params)
+ else
+ super
+ end
+end
+```
+
+This overrides a private turbo-rails method, so check it when you upgrade
+turbo-rails. Without turbo-rails, the demo app's
+[`NativeSupport`](https://github.com/inertia-native/demo-rails/blob/main/app/controllers/concerns/native_support.rb)
+concern has standalone versions of the three helpers.
+
+These helpers are for form responses. A GET link that native opens as a new
+screen would reload that screen rather than pop it.
+
## Debugging
-Pass `debug: true` to `initHotwireNative()` to log the web ↔ native message flow
+Pass `debug: true` to `initInertiaNative()` to log the web ↔ native message flow
to the web view console. Attach Safari Web Inspector (iOS) or Chrome remote
-inspect (Android) to read it.
+inspect (Android) to read it. Messages are prefixed `[inertia-native]`.
```js
-initHotwireNative({ debug: true })
+initInertiaNative({ debug: true })
```
Next: [Bridge components](/components/overview).
diff --git a/docs/reference/api.md b/docs/reference/api.md
index 315f56b..0591f7e 100644
--- a/docs/reference/api.md
+++ b/docs/reference/api.md
@@ -1,11 +1,14 @@
# API
-## `initHotwireNative`
+## `initInertiaNative`
```ts
-import { initHotwireNative } from 'inertia-native'
+import { initInertiaNative } from 'inertia-native'
-initHotwireNative(options?: { debug?: boolean }): void
+initInertiaNative(options?: {
+ debug?: boolean
+ proposeFormRedirects?: boolean
+}): void
```
Installs the `window.Turbo` shim that Hotwire Native's injected `turbo.js`
@@ -15,17 +18,25 @@ inert.
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `debug` | `boolean` | `false` | Log the web ↔ native message flow to the web view console. |
+| `proposeFormRedirects` | `boolean` | `false` | After a form lands on another URL, propose that URL to native, as Turbo does. See [Form redirects](/guide/navigation#form-redirects). |
The call is idempotent — a repeat call (HMR, double import, React StrictMode)
is a no-op.
## `useBridgeComponent`
+The same primitive ships for React, Vue and Svelte. It returns whether the
+connected native app supports a component, a `send` function, and a `restored`
+counter.
+
+### React
+
```ts
import { useBridgeComponent } from 'inertia-native/react'
useBridgeComponent(component: string): {
supported: boolean
+ restored: number
send(
event: string,
data?: Record,
@@ -34,10 +45,43 @@ useBridgeComponent(component: string): {
}
```
-Generic React wrapper over the web bridge core (`window.HotwireNative.web`).
+### Vue
+
+```ts
+import { useBridgeComponent } from 'inertia-native/vue'
+
+useBridgeComponent(component: string): {
+ supported: Ref
+ restored: Ref
+ send(event, data?, callback?): string | null
+}
+```
+
+A composable: call it from `setup`. Cleanup runs on unmount.
+
+### Svelte
+
+```ts
+import { useBridgeComponent } from 'inertia-native/svelte'
+
+useBridgeComponent(component: string): {
+ supported: Readable
+ restored: Readable
+ send(event, data?, callback?): string | null
+}
+```
+
+Call it during component initialization — it registers an `onDestroy` cleanup.
+Read the stores as `$supported` and `$restored`.
+
+### Fields
- `supported` — whether the connected native app supports `component`. Re-checks
when the native handshake completes after mount.
+- `restored` — bumped each time the web view comes back from a native screen
+ (`native:restore`, dispatched on Android). A component that draws native UI on
+ `connect` should send it again when this changes; see
+ [Restoring after a native screen](/components/overview#restoring-after-a-native-screen).
- `send` — sends a message to native. Returns the message id (for callback
cleanup), or `null` if it was queued or the component is unsupported.
@@ -58,6 +102,6 @@ The package installs and reads these on `window`:
| Global | Set by | Purpose |
| --- | --- | --- |
-| `window.Turbo` | `initHotwireNative()` | Shim that `turbo.js` drives for navigation. |
-| `window.HotwireNative.web` | `initHotwireNative()` | Web side of the bridge (`send`, `supportsComponent`, …). |
+| `window.Turbo` | `initInertiaNative()` | Shim that `turbo.js` drives for navigation. Internal. |
+| `window.HotwireNative.web` | `initInertiaNative()` | Web side of the bridge (`send`, `supportsComponent`, `removeCallback`, …). The name is Hotwire Native's bridge protocol. |
| `window.webkit.messageHandlers.turbo` | WKWebView (iOS) | Feature-detect that the app runs inside the native shell. |
diff --git a/vendor/hotwire-bridge-components b/vendor/hotwire-bridge-components
index 8b6d14c..ca539f5 160000
--- a/vendor/hotwire-bridge-components
+++ b/vendor/hotwire-bridge-components
@@ -1 +1 @@
-Subproject commit 8b6d14cebadc721efc3400795c08583c2fd89eff
+Subproject commit ca539f5013a9be7c115d1ae86eb213ffa68b154c