Today WebView Bridge
Web SDK

TypeScript

The full type surface of @todayai-labs/webview-bridge.

@todayai-labs/webview-bridge ships its own types. The complete public surface:

// ── Client ────────────────────────────────────────────────────────────────

export interface WebViewBridgeOptions {
  /** Override the global lookup name. Defaults to "todayWebViewBridge". */
  channelName?: string
  /** Inject a channel directly, bypassing the window lookup (tests / custom transports). */
  channel?: NativeChannel
}

export interface WebViewBridge {
  isAvailable(): boolean
  readonly platform: Platform
  onNativeEvent(handler: NativeEventHandler): () => void
  emit(type: string, payload?: Record<string, unknown>): void
  track(event: string, properties?: TrackProperties): void
  getHeaders(): Promise<Record<string, string>>
  refreshToken(): Promise<RefreshTokenResult>
  // Host mints the session cookie itself; the bearer is never returned to JS.
  // Resolves false with no host, an unsupported host, or a failed mint.
  establishSession(options?: { refresh?: boolean }): Promise<boolean>
  presentLiveWidgetCreation(): Promise<LiveWidgetCreationPresentationResult>
  presentNativeDialog(options: NativeDialogOptions): NativeDialogSession
  dismissNativeDialog(sessionId: string): Promise<NativeDialogDismissResult>
  fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
}

export function createWebViewBridge(options?: WebViewBridgeOptions): WebViewBridge

export const NATIVE_EVENT_DOM_TYPE = 'todayWebViewBridge:nativeEvent'
export const LIVE_WIDGET_CREATION_CAPABILITY = 'live_widget_creation.v1'
export const NATIVE_DIALOG_CAPABILITY = 'native_dialog.v1'

// ── Values ──────────────────────────────────────────────────────────────────

export type Platform = 'ios' | 'android' | 'linux' | 'macos' | 'windows' | 'web'

export type TrackProperties = Record<string, string | number | boolean>

export type NativeEventMessage = { type: string; schemaVersion?: number } & Record<string, unknown>

export type NativeEventHandler = (event: NativeEventMessage) => void

export interface RefreshTokenResult {
  authorization: string // ready-to-use header value, e.g. "Bearer <token>"
}

export interface LiveWidgetCreationPresentationResult {
  presented: boolean
}

export type NativeDialogOptions =
  | { variant: 'alert'; title?: string; message?: string; actions: NativeDialogAction[] }
  | { variant: 'confirm'; title?: string; message?: string; actions: NativeDialogAction[] }
  | {
      variant: 'prompt'
      title?: string
      message?: string
      actions: NativeDialogAction[]
      fields: NativeDialogTextField[]
    }

export interface NativeDialogAction {
  id: string
  title: string
  role?: 'default' | 'cancel' | 'destructive'
  enabled?: boolean
}

export interface NativeDialogTextField {
  id: string
  label?: string
  placeholder?: string
  initialValue?: string
  inputMode?: 'text' | 'email' | 'number' | 'decimal' | 'phone' | 'url'
  secure?: boolean
}

export 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' }

export interface NativeDialogDismissResult {
  sessionId: string
  dismissed: boolean
}
  | {
      variant: 'actionSheet'
      title?: string
      message?: string
      actions: NativeDialogAction[]
      anchor?: NativeDialogAnchorRect
    }

export interface NativeDialogSession {
  readonly sessionId: string
  readonly result: Promise<NativeDialogResult>
  dismiss(): Promise<NativeDialogDismissResult>
}

// ── Errors ────────────────────────────────────────────────────────────────

export class BridgeError extends Error {}
export class BridgeUnavailableError extends BridgeError {} // no host channel
export class BridgeUnsupportedError extends BridgeError {} // host lacks an advertised capability
export class BridgeProtocolError extends BridgeError {} // host replied with a malformed payload

The raw channel (advanced)

You normally never touch the channel directly — the client wraps it. If you are writing the native shim or a test double, this is the shape the SDK expects to find on window:

interface NativeChannel {
  // The known BridgeMessage members, or any custom typed message. An unrecognised
  // `type` resolves to `undefined` (never rejects), which is what keeps
  // capability rollout additive and forward-compatible.
  postMessage(message: BridgeMessage | BridgeCustomMessage): Promise<unknown>
}

// The wire payload field names stay `ename` / `parameters` for the analytics
// pipeline; the public `track(event, properties)` maps onto them.
type BridgeMessage =
  | { type: 'track'; ename: string; parameters?: TrackProperties }
  | { type: 'headers' }
  | { type: 'refreshToken' }
  | { type: 'establishSession'; refresh?: boolean }
  | { type: 'liveWidgetCreation.present'; schemaVersion: 1 }
  | ({ type: 'nativeDialog.present'; schemaVersion: 1; sessionId: string } & NativeDialogOptions)
  | { type: 'nativeDialog.dismiss'; schemaVersion: 1; sessionId: string }

type BridgeCustomMessage = { type: string } & Record<string, unknown>

declare global {
  interface Window {
    // iOS — WKScriptMessageHandlerWithReply
    webkit?: {
      messageHandlers?: {
        todayWebViewBridge?: NativeChannel
      }
    }
    // Android / Linux / Windows — injected shim or Electron contextBridge object
    todayWebViewBridge?: NativeChannel
    // Host-injected synchronous platform tag (authoritative for shared transports)
    __todayWebView?: { platform?: Platform }
  }
}

export {}

Both transports expose the same postMessage(message): Promise shape. That uniformity is the whole point of the channel design — see the Message envelope and Native Reference.

On this page