Skip to content
Merged
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
23 changes: 22 additions & 1 deletion docs/components/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
28 changes: 17 additions & 11 deletions docs/guide/installation.md
Original file line number Diff line number Diff line change
@@ -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).
16 changes: 9 additions & 7 deletions docs/guide/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
80 changes: 75 additions & 5 deletions docs/guide/navigation.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -11,24 +11,94 @@ how you write Inertia links or visits — the shim translates them.
| Link tap (`<Link>` / `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`, `<WhenVisible>`, `<InfiniteScroll>` and deferred props
- `async` visits, and visits with `preserveState: true` or `preserveUrl`
- visits to the current URL

Prefetches (`<Link prefetch>`, `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).
56 changes: 50 additions & 6 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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<string, unknown>,
Expand All @@ -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<boolean>
restored: Ref<number>
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<boolean>
restored: Readable<number>
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.

Expand All @@ -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. |
Loading