Native Dialog
Portable iOS and Android native dialog requests with correlated session results.
nativeDialog.present asks an iOS or Android host to present system-owned dialog UI.
It is an additive capability advertised as native_dialog.v1; Web MUST check that
capability before offering UI that depends on it.
The contract separates a dialog's semantic variant from its actions and text fields. This keeps one wire shape while covering alerts, confirmations, prompts, and action sheets.
Wire requests
type NativeDialogPresentRequest = {
type: 'nativeDialog.present'
schemaVersion: 1
sessionId: string
title?: string
message?: string
actions: NativeDialogAction[]
} & (
| { variant: 'alert' }
| { variant: 'confirm' }
| { variant: 'prompt'; fields: NativeDialogTextField[] }
| { variant: 'actionSheet'; anchor?: NativeDialogAnchorRect }
)
interface NativeDialogDismissRequest {
type: 'nativeDialog.dismiss'
schemaVersion: 1
sessionId: string
}The envelope stays flat: there is no nested payload or dialog wrapper. Titles, messages,
action titles, labels, and placeholders are already localized by Web. Native MUST NOT
translate or replace them.
Variants
| Variant | Portable native presentation |
|---|---|
alert | Informational alert with one to three actions |
confirm | Exactly two actions, exactly one with role cancel |
prompt | Alert with one to three text fields and one to three actions |
actionSheet | A native action list; optional viewport CSS-pixel anchor for iPad popover UI |
All variants require at least one non-empty action and at least one non-empty title or
message. Action and field IDs must be non-empty and unique within the request. At most one
action may have the cancel role.
interface NativeDialogAction {
id: string
title: string
role?: 'default' | 'cancel' | 'destructive'
enabled?: boolean
}
interface NativeDialogTextField {
id: string
label?: string
placeholder?: string
initialValue?: string
inputMode?: 'text' | 'email' | 'number' | 'decimal' | 'phone' | 'url'
secure?: boolean
autocapitalization?: 'none' | 'sentences' | 'words' | 'characters'
}
interface NativeDialogAnchorRect {
x: number
y: number
width: number
height: number
}anchor uses viewport CSS pixels from getBoundingClientRect(). iOS converts it into the
WKWebView coordinate space before configuring popoverPresentationController.sourceRect.
Android may ignore it. If an iPad action sheet has no usable anchor, iOS MUST present it
from a safe centered source rect rather than crash.
Session identity
The Web SDK generates a fresh UUID for every presentNativeDialog() call and exposes it
synchronously on the returned NativeDialogSession. Web does not accept a caller-provided ID,
so two SDK calls cannot accidentally reuse one.
Native MUST:
- key its pending dialog registry by the exact
sessionId; - reject a duplicate active ID at its input boundary;
- copy the ID byte-for-byte into every normal terminal result;
- settle the corresponding bridge Promise exactly once; and
- remove the pending entry before invoking the reply callback.
The SDK rejects a reply whose sessionId differs from the request with
BridgeProtocolError. The transport's private Android request ID is separate: it correlates a
single postMessage call, while sessionId identifies the user-visible dialog lifecycle across
present and dismiss.
Present result
Every normal terminal path resolves one of these unions and echoes sessionId:
type NativeDialogResult =
| {
sessionId: string
outcome: 'action'
actionId: string
values?: Record<string, string>
}
| {
sessionId: string
outcome: 'dismissed'
reason: 'cancel' | 'programmatic' | 'navigation'
}
| {
sessionId: string
outcome: 'notPresented'
reason: 'busy' | 'unavailable'
}- Tapping any declared action, including an explicit cancel action, returns
outcome: 'action'and its stableactionId. valuesis required for a non-cancel prompt action, contains every declared field ID, and is absent for all other actions.- Android Back, an allowed outside tap, or interactive action-sheet dismissal returns
dismissed/cancelwhen no declared action was selected. programmaticis the terminal result of a successfulnativeDialog.dismiss.navigationis used when the host tears down the dialog because its owning main-frame document navigated or was destroyed. If that document can no longer receive a reply, Native still cleans up the registry and must not deliver the stale result to a new document.busymeans another native dialog already owns the surface. Hosts MUST NOT queue or replace dialogs: a queued dialog can become stale before presentation.unavailablemeans the host currently has no safe presenter (UIViewController/ foregroundActivity).
Malformed requests reject with a non-secret error string. User cancellation, busy state, and a missing presenter are normal resolved outcomes, not exceptions.
Dismiss result
interface NativeDialogDismissResult {
sessionId: string
dismissed: boolean
}nativeDialog.dismiss targets one exact session. It resolves dismissed: true only when that
session was pending or active and the host initiated its dismissal. Unknown or already-settled
IDs resolve dismissed: false. The ID is always echoed. A successful dismiss also resolves the original
nativeDialog.present Promise once with dismissed/programmatic.
Web SDK
const session = bridge.presentNativeDialog({
variant: 'confirm',
title: 'Delete widget?',
message: 'This cannot be undone.',
actions: [
{ id: 'cancel', title: 'Cancel', role: 'cancel' },
{ id: 'delete', title: 'Delete', role: 'destructive' },
],
})
console.log(session.sessionId) // available before native answers
const result = await session.resultThe SDK parses both outgoing options and the unknown native reply at the bridge boundary. After that parse, application code consumes the discriminated union without repeating defensive checks.
Host scope and ownership
native_dialog.v1 is intended for trusted first-party main-frame embedded surfaces on iOS and
Android. Hosts MUST deny sub-frame calls and MUST NOT advertise it on a surface that cannot
present native UI. This is a UI authority boundary, not an authentication capability: no tokens,
native object handles, controller references, or raw platform errors cross the wire.
The page-visible capability list is feature negotiation, not a security boundary. Native must
validate the actual sender frame and trusted origin on every request using host-owned WebView
metadata; it must not trust window.__todayWebView values read back from page JavaScript.
Electron app-shell dialogs remain owned by the standardized desktop platform interfaces. They must not be routed through this embedded-WebView capability merely because the shared SDK can see the same channel shape.