@ringg/react-native 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { Room, RoomOptions } from 'livekit-client';
3
+ import { StyleProp, ViewStyle } from 'react-native';
4
+
1
5
  /**
2
6
  * Theme types for the widget.
3
7
  * Covers both widget-level theming and per-component theming.
@@ -77,6 +81,28 @@ interface SlashCommand {
77
81
  description: string;
78
82
  }
79
83
 
84
+ /**
85
+ * GENERATED FILE — DO NOT EDIT.
86
+ * Source: tokens/tokens.json + tokens/defaults.json
87
+ * Regenerate: node scripts/generate-tokens.mjs
88
+ */
89
+
90
+ declare const DEFAULT_WIDGET_THEME: ResolvedWidgetTheme;
91
+ declare const WIDGET_PRESET_THEMES: Record<string, WidgetTheme>;
92
+ declare const WIDGET_DEFAULTS: {
93
+ readonly mode: "prod";
94
+ readonly title: "Ringg AI Support";
95
+ readonly description: "Ringg AI offers 24/7 voice support to handle your business calls efficiently and professionally";
96
+ readonly defaultTab: "audio";
97
+ readonly hideTabSelector: false;
98
+ readonly defaultExpanded: false;
99
+ readonly bypassStartScreen: false;
100
+ readonly bypassFeedbackScreen: false;
101
+ readonly voiceCallShowAnimation: false;
102
+ readonly voiceCallShowTranscript: true;
103
+ readonly notificationTuneUrl: "https://assets.ringg.ai/audios/misc/widget_notification.mp3";
104
+ };
105
+
80
106
  /**
81
107
  * Widget configuration types.
82
108
  * These define the public API surface for consumers initializing the widget.
@@ -177,6 +203,16 @@ interface RinggWidgetConfig {
177
203
  xApiKey?: string;
178
204
  /** JWT for authentication (Authorization header) — wins over xApiKey when both are set */
179
205
  authorization?: string;
206
+ /**
207
+ * `Origin` to present to the backend, which allow-lists an agent's callers
208
+ * by that value. Browsers send it themselves, so web leaves this unset and
209
+ * MUST NOT set it — `Origin` is a forbidden header there. Native callers
210
+ * send nothing on their own and have to supply their own identity, by
211
+ * convention `<platform>://<bundleId>` (see @ringg/react-native's
212
+ * `appOrigin`). Whatever string is given is sent verbatim; only the backend
213
+ * decides whether it is allowed.
214
+ */
215
+ clientOrigin?: string;
180
216
  /** Custom variables passed to the agent (e.g., user name, role) */
181
217
  variables?: WidgetVariables;
182
218
  /** Environment mode — selects which UrlResolver entry to use */
@@ -249,6 +285,11 @@ interface CalendarBookingPayload extends BaseComponent {
249
285
  component_type: "calendar_booking";
250
286
  data: CalendarBookingData;
251
287
  }
288
+ interface GroupedSlots {
289
+ date: string;
290
+ dateLabel: string;
291
+ slots: CalendarSlot[];
292
+ }
252
293
  type FormFieldType = "text" | "email" | "tel" | "number" | "select" | "multiselect" | "textarea" | "date" | "boolean";
253
294
  interface FormFieldOption {
254
295
  value: string;
@@ -547,6 +588,8 @@ interface EventBus {
547
588
  /** Remove all listeners for one event, or all events if omitted. */
548
589
  off(event?: WidgetEventName): void;
549
590
  }
591
+ /** Web implementation — dispatches/listens via DOM CustomEvents on `window`. */
592
+ declare function createDomEventBus(): EventBus;
550
593
  /** In-memory event bus — for React Native or testing. */
551
594
  declare function createCallbackEventBus(): EventBus;
552
595
 
@@ -564,6 +607,7 @@ interface Clock {
564
607
  setTimeout(fn: () => void, ms: number): TimerHandle;
565
608
  clearTimeout(handle: TimerHandle): void;
566
609
  }
610
+ declare function createSystemClock(): Clock;
567
611
 
568
612
  /**
569
613
  * Small platform ports — capabilities core needs but cannot implement
@@ -580,6 +624,9 @@ interface NotificationPlayer {
580
624
  /** Best-effort playback; must never throw (autoplay policies etc.) */
581
625
  play(): void;
582
626
  }
627
+ /** No-op defaults for platforms/tests that don't wire these up */
628
+ declare const grantedMicPermission: MicPermissionPort;
629
+ declare const silentNotificationPlayer: NotificationPlayer;
583
630
 
584
631
  /**
585
632
  * API client for the Ringg backend.
@@ -595,6 +642,101 @@ interface EnvironmentUrls {
595
642
  interface UrlResolver {
596
643
  resolve(mode: EnvironmentMode): EnvironmentUrls;
597
644
  }
645
+ declare function createStaticUrlResolver(urls: Record<EnvironmentMode, EnvironmentUrls>): UrlResolver;
646
+ interface WebcallResponse {
647
+ call_id: string;
648
+ user_token: string;
649
+ enabled_slash_commands?: SlashCommand[];
650
+ }
651
+ interface FeedbackPayload {
652
+ rating: number;
653
+ comment: string;
654
+ }
655
+ interface ApiClientConfig {
656
+ urlResolver: UrlResolver;
657
+ mode: EnvironmentMode;
658
+ /** Sent as X-API-KEY */
659
+ xApiKey?: string;
660
+ /** Sent as Authorization; wins over xApiKey when both are set */
661
+ authorization?: string;
662
+ /** Sent as `Origin`. Omitted entirely when unset — see RinggWidgetConfig. */
663
+ clientOrigin?: string;
664
+ }
665
+ interface RinggApiClient {
666
+ readonly backendUrl: string;
667
+ readonly livekitUrl: string;
668
+ /** Initiate a webcall — returns the LiveKit token and call ID. */
669
+ startWebcall(params: {
670
+ agentId: string;
671
+ variables: WidgetVariables;
672
+ mediaType: "audio" | "text";
673
+ }): Promise<WebcallResponse>;
674
+ /** Submit post-call feedback. Best-effort — callers decide error handling. */
675
+ submitFeedback(callId: string, feedback: FeedbackPayload): Promise<void>;
676
+ /** Call a component API endpoint (for interactive flow on_complete actions). */
677
+ callComponentApi(method: "POST" | "GET", endpoint: string, payload: Record<string, string>): Promise<ComponentActionResponse>;
678
+ }
679
+ declare function createRinggApiClient(config: ApiClientConfig): RinggApiClient;
680
+
681
+ /**
682
+ * Theme engine — resolves, merges, and provides theme values.
683
+ * Pure functions with no framework dependency.
684
+ *
685
+ * Default and preset theme VALUES live in tokens/tokens.json (shared across
686
+ * all implementations via codegen) — only merging/contrast LOGIC lives here.
687
+ */
688
+
689
+ /** Merge a partial user theme with the defaults. Returns a fully resolved theme. */
690
+ declare function mergeWidgetTheme(theme?: WidgetTheme): ResolvedWidgetTheme;
691
+ /** Merge a component-level theme with the widget theme as fallback. */
692
+ declare function mergeComponentTheme(componentTheme?: ComponentTheme, widgetTheme?: ResolvedWidgetTheme): ComponentTheme;
693
+ /** Get button border radius based on style */
694
+ declare function getButtonRadius(style?: ButtonStyle): string;
695
+ /** Check if a color is considered "light" (for auto-contrast) */
696
+ declare function isLightColor(hexColor: string): boolean;
697
+ /** Get a contrasting text color (black or white) for a given background */
698
+ declare function getContrastingTextColor(bgColor: string): string;
699
+
700
+ /**
701
+ * Gradient-aware style utilities for the widget theme system.
702
+ *
703
+ * CSS `backgroundColor` does not support gradient values — only the
704
+ * `background` shorthand does. These helpers detect whether a color value is
705
+ * a solid hex/rgb or a CSS gradient string, and extract a solid fallback
706
+ * color for surfaces that can't render gradients (borders, React Native).
707
+ *
708
+ * Parsing is deliberately explicit — small named steps instead of regexes —
709
+ * so a bad input can be walked through in a debugger function by function.
710
+ */
711
+ /** Detect whether a CSS color value is a gradient string. */
712
+ declare function isGradient(color: string): boolean;
713
+ /** Extract all color stops from a gradient string, in order. */
714
+ declare function extractColorsFromGradient(gradient: string): string[];
715
+ /**
716
+ * Return the dominant (first) color from a gradient, or the color as-is for
717
+ * solid values. Falls back to the original string if no colors can be extracted.
718
+ */
719
+ declare function getDominantColor(color: string): string;
720
+ /**
721
+ * Determine the correct CSS properties for a background color.
722
+ * Returns `{ backgroundColor }` for solid colors, `{ background }` for gradients.
723
+ * On React Native, gradients aren't natively supported — use `getDominantColor()`
724
+ * to fall back to a solid color.
725
+ */
726
+ declare function colorToBackground(color: string): {
727
+ backgroundColor?: string;
728
+ background?: string;
729
+ };
730
+ /**
731
+ * Always return a solid hex/rgb color string. Use for properties that don't
732
+ * support gradients: border, box-shadow, accent-color.
733
+ */
734
+ declare function colorToSolid(color: string): string;
735
+ /**
736
+ * Return the correct CSS properties for a border color.
737
+ * Solid colors -> `{ borderColor }`, gradients -> `{ borderImage, borderStyle }`.
738
+ */
739
+ declare function colorToBorder(color: string): Record<string, string>;
598
740
 
599
741
  /**
600
742
  * RPC message formatting — transforms between backend RPC format and internal
@@ -608,12 +750,61 @@ interface UrlResolver {
608
750
  * This module handles both directions.
609
751
  */
610
752
 
753
+ /**
754
+ * Transform a backend RPC payload into the internal component format.
755
+ * Returns null if the component type is unknown.
756
+ */
757
+ declare function transformRpcToComponent(componentName: string, rpcPayload: Record<string, unknown>): SimpleComponentPayload | InteractiveFlowPayload | null;
758
+ interface RpcMessage {
759
+ method: string;
760
+ payload: string;
761
+ }
762
+ /**
763
+ * Format a component response payload for sending back to the backend via RPC.
764
+ * Used when the user interacts with a component (selects a slot, submits a
765
+ * form, taps a button).
766
+ */
767
+ declare function formatComponentResponse(componentName: string, componentId: string, responseData: Record<string, unknown>, originalConfig: unknown): RpcMessage;
768
+ /** Format a slash command for sending via RPC. */
769
+ declare function formatSlashCommand(command: string): RpcMessage;
611
770
  /** A Block Kit action as it goes on the wire (the UI-only `label` never does). */
612
771
  interface BlocksActionWire {
613
772
  action_id: string;
614
773
  value?: unknown;
615
774
  values: Record<string, unknown>;
616
775
  }
776
+ /**
777
+ * Format a Block Kit interaction for the agent. Envelope mirrors the dashboard
778
+ * test widget the bot bridge was tested against: `component_id` and `values`
779
+ * are always present; `tool_id` is omitted when falsy and `value` when
780
+ * undefined (never sent as null/empty keys).
781
+ */
782
+ declare function formatBlocksAction(toolId: string | undefined, componentId: string, action: BlocksActionWire): RpcMessage;
783
+ /**
784
+ * Format a raw dynamic-data payload (client packs: disease search/submit).
785
+ * The payload already carries its own `component_type` envelope.
786
+ */
787
+ declare function formatDynamicData(payload: Record<string, unknown>): RpcMessage;
788
+
789
+ /**
790
+ * Component data helpers — pure functions for working with interactive
791
+ * component data. Shared across web and React Native renderers.
792
+ */
793
+
794
+ /** Group calendar slots by date for display */
795
+ declare function groupSlotsByDate(slots: CalendarSlot[]): GroupedSlots[];
796
+ /** Format a slot time for display */
797
+ declare function formatSlotTime(datetime: string, timezone?: string): string;
798
+ /** Format a slot date for display */
799
+ declare function formatSlotDate(datetime: string, timezone?: string): string;
800
+ /** Interpolate a payload template with collected values */
801
+ declare function buildPayload(template: Record<string, string>, values: Record<string, string>): Record<string, string>;
802
+ declare function isCalendarBooking(payload: ComponentPayload): payload is CalendarBookingPayload;
803
+ declare function isForm(payload: ComponentPayload): payload is FormPayload;
804
+ declare function isButtons(payload: ComponentPayload): payload is ButtonsPayload;
805
+ declare function isConfirmation(payload: ComponentPayload): payload is ConfirmationPayload;
806
+ declare function isInteractiveFlow(payload: ComponentPayload): payload is InteractiveFlowPayload;
807
+ declare function isBlocks(payload: ComponentPayload): payload is BlocksPayload;
617
808
 
618
809
  /**
619
810
  * Base store — the one state-sharing primitive in core.
@@ -631,6 +822,14 @@ interface Store<T> {
631
822
  getSnapshot(): T;
632
823
  subscribe(listener: (snapshot: T) => void): () => void;
633
824
  }
825
+ /** Internal store handle: the public read API plus commit/dispose. */
826
+ interface StoreHandle<T> extends Store<T> {
827
+ /** Rebuild the immutable snapshot from current state and notify subscribers. */
828
+ commit(): void;
829
+ /** Drop all subscribers. */
830
+ dispose(): void;
831
+ }
832
+ declare function createStore<T>(build: () => T): StoreHandle<T>;
634
833
 
635
834
  /**
636
835
  * Shell store — widget open/close/feedback lifecycle and the active call id.
@@ -663,6 +862,7 @@ interface ShellStore extends Store<ShellSnapshot> {
663
862
  setCurrentCallId(callId: string | null): void;
664
863
  dispose(): void;
665
864
  }
865
+ declare function createShellStore(eventBus: EventBus, initialCallMode?: MediaType, defaultExpanded?: boolean): ShellStore;
666
866
 
667
867
  /**
668
868
  * Session store — the call lifecycle state machine.
@@ -710,6 +910,7 @@ interface SessionStore extends Store<SessionSnapshot> {
710
910
  clearError(): void;
711
911
  dispose(): void;
712
912
  }
913
+ declare function createSessionStore(api: RinggApiClient, transport: TransportAdapter, mic: MicPermissionPort): SessionStore;
713
914
 
714
915
  /**
715
916
  * Message store — the single ordered conversation timeline.
@@ -768,6 +969,7 @@ interface MessageStore extends Store<MessageSnapshot> {
768
969
  reset(): void;
769
970
  dispose(): void;
770
971
  }
972
+ declare function createMessageStore(clock: Clock): MessageStore;
771
973
 
772
974
  /**
773
975
  * Typing store — drives the "agent is typing" indicator (text mode).
@@ -798,6 +1000,7 @@ interface TypingStore extends Store<TypingSnapshot> {
798
1000
  waitMinDuration(): Promise<void>;
799
1001
  dispose(): void;
800
1002
  }
1003
+ declare function createTypingStore(clock: Clock): TypingStore;
801
1004
 
802
1005
  /**
803
1006
  * Component store — interactive components pushed by the backend mid-call.
@@ -845,6 +1048,9 @@ interface ComponentStore extends Store<ComponentSnapshot> {
845
1048
  reset(): void;
846
1049
  dispose(): void;
847
1050
  }
1051
+ declare function createComponentStore(clock: Clock,
1052
+ /** Commits a component to the timeline — wired to MessageStore + host events by the controller */
1053
+ commitComponent: (component: ComponentPayload) => void): ComponentStore;
848
1054
 
849
1055
  /**
850
1056
  * Slash command store — merges commands declared in config with commands the
@@ -860,6 +1066,22 @@ interface SlashCommandStore extends Store<SlashCommandSnapshot> {
860
1066
  reset(): void;
861
1067
  dispose(): void;
862
1068
  }
1069
+ declare function createSlashCommandStore(configCommands?: SlashCommand[]): SlashCommandStore;
1070
+
1071
+ /**
1072
+ * Timing constants — every value encodes a UX bug that was found and fixed in
1073
+ * the production widget. Values live in tokens/defaults.json (shared across
1074
+ * all implementations via codegen); change them there, never here.
1075
+ */
1076
+ /** Chat-widget RPCs land before the agent's text streams in; buffer them this
1077
+ * long waiting for the text so the widget sorts below it. */
1078
+ declare const CHAT_WIDGET_GRACE_MS: number;
1079
+ /** Once the agent text lands, release buffered widgets after this beat so the
1080
+ * message paints first. */
1081
+ declare const CHAT_WIDGET_AFTER_TEXT_MS: number;
1082
+ /** Keep the typing dots visible at least this long — instant replies
1083
+ * otherwise make the indicator flash. */
1084
+ declare const MIN_TYPING_INDICATOR_MS: number;
863
1085
 
864
1086
  /**
865
1087
  * RinggWidgetController — the composition root of the widget brain.
@@ -961,4 +1183,438 @@ declare const useRinggShell: (controller: RinggWidgetController) => ShellSnapsho
961
1183
  declare const useRinggComponents: (controller: RinggWidgetController) => ComponentSnapshot;
962
1184
  declare const useRinggSlashCommands: (controller: RinggWidgetController) => SlashCommandSnapshot;
963
1185
 
964
- export { type ControllerPorts, type RinggWidgetConfig, type RinggWidgetController, type TransportAdapter, createCallbackEventBus, createRinggWidgetController, useRinggComponents, useRinggMessages, useRinggSession, useRinggShell, useRinggSlashCommands, useRinggTyping, useStoreSnapshot };
1186
+ /**
1187
+ * RinggWidget — the assembled React Native widget.
1188
+ *
1189
+ * The native counterpart of the web assembly: the same tree, the same order,
1190
+ * the same branches. It holds NO conversation state — every snapshot comes
1191
+ * from `@ringg/core` via the shared hooks and every intent is forwarded to the
1192
+ * controller. If a behaviour looks like it belongs here, it belongs in core.
1193
+ *
1194
+ * Three things genuinely differ from web, all forced by the platform:
1195
+ *
1196
+ * 1. There is no `position: fixed`. The widget is an absolutely-filled
1197
+ * overlay with `pointerEvents="box-none"`, so taps pass through everywhere
1198
+ * except the trigger and the panel. Mount it as the LAST child of the app
1199
+ * root; `widgetPosition` (a web-only config) is ignored and the panel
1200
+ * sizes itself to the device.
1201
+ * 2. The keyboard covers the bottom of the screen. The panel lives inside a
1202
+ * `KeyboardAvoidingView` so the composer stays visible while typing —
1203
+ * without it the input is simply unreachable in text mode.
1204
+ * 3. The transcript cannot measure a child's offset the way web does to
1205
+ * anchor the newest turn's first line. It pins to the bottom instead, and
1206
+ * the "new message" / "typing toggled" distinction is preserved so a long
1207
+ * reply arriving does not yank the view while the user is reading.
1208
+ */
1209
+
1210
+ /**
1211
+ * The managed form: hand it a config and it owns everything underneath.
1212
+ *
1213
+ * This is the RN counterpart of web's `loadAgent({ agentId, authorization })`.
1214
+ * The transport, the endpoint table and the microphone port are ours, not the
1215
+ * integrator's, so the app does not carry our infrastructure in its source.
1216
+ */
1217
+ interface ManagedRinggWidgetProps {
1218
+ config: RinggWidgetConfig;
1219
+ /**
1220
+ * Overrides for the ports the widget wires by default. Supply `transport` to
1221
+ * bring your own (a mock in tests, a shared room), `urlResolver` to point at
1222
+ * something other than the Ringg endpoints, `notification` to give the widget
1223
+ * a sound, or `onDomAction` to handle agent-triggered app actions.
1224
+ */
1225
+ ports?: Partial<ControllerPorts>;
1226
+ /**
1227
+ * Called once with the controller the widget built, for hosts that want to
1228
+ * listen for events or drive the panel imperatively without owning its
1229
+ * lifecycle. The controller is destroyed on unmount — do not keep using it.
1230
+ */
1231
+ onReady?: (controller: RinggWidgetController) => void;
1232
+ controller?: never;
1233
+ room?: never;
1234
+ }
1235
+ /**
1236
+ * The controlled form: you built the controller, you own its lifecycle.
1237
+ *
1238
+ * Reach for it when the widget cannot own the transport — a custom adapter, a
1239
+ * room shared with the rest of the app, or a headless setup where the widget is
1240
+ * one of several views on one controller.
1241
+ */
1242
+ interface ControlledRinggWidgetProps {
1243
+ controller: RinggWidgetController;
1244
+ /**
1245
+ * The LiveKit room from `createLiveKitTransport()`. Optional: it only powers
1246
+ * the in-call audio visualizer and the mute button. Without it the widget is
1247
+ * fully functional and the visualizer shows its ambient idle animation.
1248
+ */
1249
+ room?: Room;
1250
+ config?: never;
1251
+ ports?: never;
1252
+ onReady?: never;
1253
+ }
1254
+ type RinggWidgetProps = ManagedRinggWidgetProps | ControlledRinggWidgetProps;
1255
+ /**
1256
+ * The widget. Pass `config` and it owns the transport, endpoints and
1257
+ * microphone; pass `controller` when you built those yourself.
1258
+ */
1259
+ declare const RinggWidget: FC<RinggWidgetProps>;
1260
+
1261
+ /**
1262
+ * LiveKit React Native transport — the real `TransportAdapter` over
1263
+ * `@livekit/react-native`, behavior-matched to the web adapter (which is
1264
+ * itself matched to production, @desivocal/agents-cdn).
1265
+ *
1266
+ * The wire behavior is deliberately identical to `web/src/transport/
1267
+ * livekit-transport.ts` — same Room options, same double-send chat, same
1268
+ * agent-classification and session-ended rules — because both talk to the same
1269
+ * backend and core drives them through the same port. Read that file's parity
1270
+ * notes; they apply here verbatim.
1271
+ *
1272
+ * Only what the platform forces differs:
1273
+ * - remote audio needs no elements. Mobile plays subscribed audio through the
1274
+ * OS, so `<RoomAudioRenderer />`'s job becomes owning an audio SESSION
1275
+ * (see platform/audio-session.ts) rather than attaching media elements;
1276
+ * - `registerGlobals()` must run before a Room is constructed — the adapter
1277
+ * does it so a missing app-entry call is not a mysterious runtime failure;
1278
+ * - autoplay policy has no mobile equivalent, so the web adapter's
1279
+ * `startAudio()` recovery has no counterpart.
1280
+ */
1281
+
1282
+ interface LiveKitTransportOptions {
1283
+ /**
1284
+ * Extra LiveKit `RoomOptions` merged over the parity defaults
1285
+ * (`dynacast: true, adaptiveStream: true`). Rarely needed.
1286
+ */
1287
+ roomOptions?: RoomOptions;
1288
+ /**
1289
+ * Own the native audio session for the duration of a call — the mobile
1290
+ * equivalent of production's `<RoomAudioRenderer />`. Default true; disable
1291
+ * when the host app already manages an audio session (an in-app player, a
1292
+ * CallKit/ConnectionService integration).
1293
+ */
1294
+ manageAudioSession?: boolean;
1295
+ /**
1296
+ * Call `registerGlobals()` before constructing the Room. Default true.
1297
+ * Set false when the app already calls it at its entry point — it is
1298
+ * idempotent, so this is a formality rather than a correctness switch.
1299
+ */
1300
+ registerGlobals?: boolean;
1301
+ }
1302
+ interface LiveKitTransport {
1303
+ transport: TransportAdapter;
1304
+ /**
1305
+ * The underlying Room, for presentational concerns the port cannot express
1306
+ * (visualizer track handles, `isMicrophoneEnabled` readback — spec §11).
1307
+ * State-changing calls must keep going through the transport/core.
1308
+ */
1309
+ room: Room;
1310
+ /** Tears down listeners, handlers and the audio session; disconnects the room. */
1311
+ dispose(): void;
1312
+ }
1313
+ declare const createLiveKitTransport: (options?: LiveKitTransportOptions) => LiveKitTransport;
1314
+
1315
+ /**
1316
+ * The Ringg-hosted endpoints, baked into the platform entry point.
1317
+ *
1318
+ * Core deliberately embeds no URLs — they arrive through a `UrlResolver` the
1319
+ * entry point supplies. Every other entry point already carries this table:
1320
+ * web bundles it in `cdn.tsx` so `loadAgent({ agentId, authorization })` needs
1321
+ * nothing else, and Flutter exposes it as `defaultUrlResolver`. React Native
1322
+ * was the only one asking integrators to type endpoints into their own app,
1323
+ * which leaked our infrastructure into their source for no benefit.
1324
+ *
1325
+ * `mode` selects the row; it defaults to `prod`.
1326
+ */
1327
+
1328
+ declare const DEFAULT_ENVIRONMENT_URLS: Record<EnvironmentMode, EnvironmentUrls>;
1329
+ /** `ControllerPorts.urlResolver` default for the managed `<RinggWidget config />`. */
1330
+ declare const defaultUrlResolver: UrlResolver;
1331
+
1332
+ /**
1333
+ * Native audio session — React Native's replacement for the web adapter's
1334
+ * hidden `<audio>` elements.
1335
+ *
1336
+ * On web, remote audio needs a DOM element to play into. On mobile it needs
1337
+ * the opposite: nothing to attach, but an OS-level audio session that has to
1338
+ * be configured and activated before the call and released after it, or the
1339
+ * mic indicator stays lit and the app keeps ducking other audio.
1340
+ *
1341
+ * The configuration encodes the mobile audio invariants (AGENTS.md rule 10):
1342
+ * communication mode so the call survives backgrounding and routes through the
1343
+ * earpiece/bluetooth stack rather than the media stream, and a preferred
1344
+ * output list that follows a headset or bluetooth device when one appears.
1345
+ *
1346
+ * Every call is best-effort. A device that refuses to hand over the audio
1347
+ * session must not take the call down with it — the user still gets a
1348
+ * (possibly routed-oddly) conversation, which beats a hard failure.
1349
+ */
1350
+ interface AudioSessionPort {
1351
+ /** Configure + activate. Safe to call repeatedly; only the first wins. */
1352
+ start(): Promise<void>;
1353
+ /** Release the session. Safe to call when never started. */
1354
+ stop(): Promise<void>;
1355
+ }
1356
+ declare const createAudioSession: () => AudioSessionPort;
1357
+
1358
+ /**
1359
+ * React Native `MicPermissionPort`.
1360
+ *
1361
+ * Core's session store drives this on audio start: `isGranted()` first, then
1362
+ * `request()`, aborting the call on denial — identical to web. What differs is
1363
+ * how the answer is obtained, and the two platforms genuinely differ:
1364
+ *
1365
+ * - Android exposes a real permission API, so the state can be READ without
1366
+ * prompting. That matters: `isGranted()` must never show a dialog, or the
1367
+ * user gets prompted before they have pressed anything.
1368
+ * - iOS has no readable pre-check from JS. The only probe is `getUserMedia`,
1369
+ * which prompts on first use — the same fallback the web port uses for
1370
+ * browsers without the Permissions API. Once answered, iOS resolves the
1371
+ * probe from its own record without prompting again.
1372
+ *
1373
+ * The probe must never keep the microphone: every track it opens is stopped
1374
+ * before returning, or the OS recording indicator stays lit before the call
1375
+ * has even started.
1376
+ */
1377
+
1378
+ declare const createNativeMicPermission: () => MicPermissionPort;
1379
+
1380
+ /**
1381
+ * App identity for the backend's caller allow-list.
1382
+ *
1383
+ * The Ringg backend allow-lists an agent's callers by `Origin`. A browser
1384
+ * attaches it automatically; a native app sends nothing, so it has to present
1385
+ * its own identity — by convention `<platform>://<bundleId>`, the scheme the
1386
+ * backend accepts for app clients. Without it the webcall request is rejected
1387
+ * before authentication even matters (`400 Origin header is required`).
1388
+ *
1389
+ * The bundle id is the INTEGRATOR's, not ours, so the value allow-listed in
1390
+ * the dashboard is their app. React Native cannot read it without a native
1391
+ * module, and this package refuses to grow one for a single string — so the id
1392
+ * is a parameter. Expo apps have it in `expo-application`'s `applicationId`;
1393
+ * bare apps already know their own.
1394
+ *
1395
+ * Mirrors `flutter/lib/src/platform/app_origin.dart`, which derives the same
1396
+ * string from `package_info_plus`.
1397
+ */
1398
+ /**
1399
+ * `<platform>://<bundleId>` for the running app — e.g. `android://com.acme.app`.
1400
+ *
1401
+ * Returns undefined on a platform the backend has no scheme for, which
1402
+ * includes react-native-web: there the browser sends a real `Origin` and
1403
+ * setting one from JavaScript is forbidden anyway.
1404
+ */
1405
+ declare const appOrigin: (bundleId: string) => string | undefined;
1406
+
1407
+ /**
1408
+ * React Native `NotificationPlayer`.
1409
+ *
1410
+ * Core decides WHEN to play (an agent reply or component landing while the
1411
+ * widget is closed); the port only owns the how. On web that is one line —
1412
+ * `new Audio(url)`. RN has no audio playback in the platform at all: every
1413
+ * option (expo-audio, react-native-sound, react-native-video) is a native
1414
+ * module, and forcing one on integrators would mean an extra pod/gradle
1415
+ * dependency and an Expo config plugin for a notification chime.
1416
+ *
1417
+ * So the decision is inverted: the widget ships silent by default and the
1418
+ * integrator opts in with three lines against whichever player their app
1419
+ * already has. `notificationTuneUrl` from the config is handed to that player,
1420
+ * so the URL stays a config concern on every platform.
1421
+ */
1422
+
1423
+ /** What an integrator's audio library has to be able to do. */
1424
+ type PlayTune = (tuneUrl: string) => void;
1425
+ /**
1426
+ * The default. Silent, and deliberately so — a widget that cannot find an
1427
+ * audio library should be quiet, not noisy about it on every reply.
1428
+ */
1429
+ declare const createSilentNotificationPlayer: () => NotificationPlayer;
1430
+ /**
1431
+ * Bridges the config's tune URL to a host-provided player.
1432
+ *
1433
+ * ```ts
1434
+ * import { createAudioPlayer } from "expo-audio";
1435
+ * const notifications = createNotificationPlayer(DEFAULT_CONFIG.notificationTuneUrl, (url) => createAudioPlayer(url).play());
1436
+ * ```
1437
+ *
1438
+ * Playback failures are swallowed: a chime is best-effort by contract, and a
1439
+ * rejected promise here must never surface mid-conversation.
1440
+ */
1441
+ declare const createNotificationPlayer: (tuneUrl: string, playTune: PlayTune) => NotificationPlayer;
1442
+
1443
+ /**
1444
+ * Host action dispatcher — the React Native counterpart of the web widget's
1445
+ * `executeDomAction`.
1446
+ *
1447
+ * Core normalises the `execute_dom_action` RPC into a `DomAction` and hands it
1448
+ * to the `onDomAction` port. On web that ends in `window.dispatchEvent`, which
1449
+ * works because the host page is already an event bus. RN has no such ambient
1450
+ * bus, so the integrator supplies the handler and gets the event name plus the
1451
+ * payload the agent sent — the same `{...default_payload, action_id}` detail
1452
+ * the web CustomEvent carries, so a shared backend config drives both.
1453
+ *
1454
+ * Validation, logging and the "unknown kind" path stay identical to web: those
1455
+ * are contract behaviour, not platform detail.
1456
+ */
1457
+
1458
+ /** What the host receives — the web CustomEvent, minus the DOM. */
1459
+ interface HostAction {
1460
+ /** The agent-configured event name (`CustomEvent.type` on web). */
1461
+ name: string;
1462
+ /** `default_payload` with the action id folded in (`CustomEvent.detail`). */
1463
+ payload: Record<string, unknown>;
1464
+ }
1465
+ type HostActionHandler = (action: HostAction) => void;
1466
+ /**
1467
+ * Builds the `onDomAction` port. `onAction` runs in the caller's context and
1468
+ * is allowed to throw — a broken host handler is reported, never fatal.
1469
+ */
1470
+ declare const createHostActionDispatcher: (onAction: HostActionHandler) => (action: DomAction | undefined | null, onLog?: DomActionLogger) => void;
1471
+
1472
+ /**
1473
+ * Widget theme for React Native.
1474
+ *
1475
+ * Deliberately thin: the theme VALUES (defaults, presets) and the merge/
1476
+ * contrast LOGIC both come from `@ringg/core` — `mergeWidgetTheme` here is
1477
+ * core's function, not a copy. The web package still carries its own copy of
1478
+ * that table for byte-fidelity with agents-cdn; RN has no such legacy to match
1479
+ * and takes the shared one, so a token change reaches this platform through
1480
+ * codegen (AGENTS.md rule 2).
1481
+ *
1482
+ * What IS local is unit translation. The theme surface is CSS-shaped
1483
+ * (`"16px"`, `"9999px"`, `fontFamily: "inherit"`) because that's the contract
1484
+ * integrators already write against on web; RN needs numbers and a real family
1485
+ * name, so the `resolve*` helpers below are the single conversion point.
1486
+ */
1487
+
1488
+ interface WidgetThemeProviderProps {
1489
+ theme?: WidgetTheme;
1490
+ children: ReactNode;
1491
+ }
1492
+ /** `"16px"` → `16`. Unparseable values fall back rather than laying out as NaN. */
1493
+ declare const resolveRadius: (value: string | undefined, fallback?: number) => number;
1494
+ /** Button corner radius as an RN number, from core's shared style → CSS mapping. */
1495
+ declare const resolveButtonRadius: (style: ButtonStyle | undefined) => number;
1496
+ /**
1497
+ * `"inherit"` has no meaning in RN — there is no cascade. Returning undefined
1498
+ * lets `<Text>` fall through to the platform system font, which is what
1499
+ * "inherit" resolves to on web for an unstyled host page.
1500
+ */
1501
+ declare const resolveFontFamily: (fontFamily: string | undefined) => string | undefined;
1502
+ declare const WidgetThemeProvider: FC<WidgetThemeProviderProps>;
1503
+ declare const useWidgetTheme: () => ResolvedWidgetTheme;
1504
+
1505
+ /**
1506
+ * Gradient fills for React Native.
1507
+ *
1508
+ * The theme surface is CSS-shaped — `primaryColor` may be a solid hex OR a
1509
+ * `linear-gradient(...)` string (see core `types/theme.ts`, and the gradient
1510
+ * presets in `widget-theme`). The DOM renders those for free; RN has no
1511
+ * gradient primitive at all, so every themed surface goes through here.
1512
+ *
1513
+ * Colour PARSING is not duplicated — `isGradient` / `extractColorsFromGradient`
1514
+ * / `getDominantColor` come from `@ringg/core`, the same functions the web
1515
+ * widget uses (AGENTS.md rule 2: shared values come from core, never a local
1516
+ * re-implementation). Only the two things core cannot know about — the angle in
1517
+ * SVG coordinates, and how to paint it — live in this file.
1518
+ *
1519
+ * Non-linear gradients (radial/conic) degrade to their dominant colour rather
1520
+ * than approximating badly; the theme presets are all linear.
1521
+ */
1522
+
1523
+ interface GradientFillProps {
1524
+ /** A theme colour: solid (`#0a0a0b`, `rgb(...)`) or a CSS gradient string. */
1525
+ color: string;
1526
+ /** Applied to the wrapper — size, padding, radius, alignment. */
1527
+ style?: StyleProp<ViewStyle>;
1528
+ children?: ReactNode;
1529
+ }
1530
+ /**
1531
+ * Paints `color` behind `children`. Solid colours take the cheap path
1532
+ * (`backgroundColor`); gradients get an absolutely-filled SVG layer that
1533
+ * inherits the wrapper's rounding through `overflow: hidden`.
1534
+ */
1535
+ declare const GradientFill: FC<GradientFillProps>;
1536
+ /**
1537
+ * A guaranteed-solid colour, for the properties RN can only paint flat —
1538
+ * borders, shadows, icon tints, status dots.
1539
+ */
1540
+ declare const solidColor: (color: string) => string;
1541
+
1542
+ /**
1543
+ * `PortableStyles` → React Native styles.
1544
+ *
1545
+ * `PortableStyles` (core `types/config.ts`) is the deliberately small subset of
1546
+ * CSS the config surface accepts precisely so it can cross platforms. Web hands
1547
+ * the values straight to the DOM; RN cannot — it has no CSS parser, so a
1548
+ * `"16px"` string silently breaks layout instead of throwing.
1549
+ *
1550
+ * This is the one place that converts. Rules, matching the Dart port
1551
+ * (`flutter/lib/src/ui/widgets/portable_styles.dart`):
1552
+ * - numbers pass through as density-independent pixels;
1553
+ * - `"16px"` / `"16"` → `16`;
1554
+ * - `"50%"` survives only where RN accepts percentages (width/height);
1555
+ * - anything else (`calc()`, `em`, `auto`, gradients in `backgroundColor`) is
1556
+ * dropped rather than guessed at — a dropped style degrades, a wrong one
1557
+ * corrupts the layout.
1558
+ */
1559
+
1560
+ /** Convert a config `PortableStyles` bag into an RN `ViewStyle`. */
1561
+ declare const toViewStyle: (styles: PortableStyles | undefined) => ViewStyle;
1562
+ /** Icon sizing from `ButtonIconConfig.size` (same parsing, single value). */
1563
+ declare const toSize: (value: string | number | undefined, fallback: number) => number;
1564
+
1565
+ /**
1566
+ * Markdown for chat bubbles.
1567
+ *
1568
+ * The web widget hands agent replies to `react-markdown` + `remark-gfm` and
1569
+ * lets Tailwind's prose plugin style the result. Neither of those crosses to
1570
+ * RN, and this package takes no dependency for it, so what lives here is a
1571
+ * renderer for the subset that actually appears in agent replies. Everything
1572
+ * else falls through as the literal text the model wrote — a reply must never
1573
+ * show half-parsed markup.
1574
+ *
1575
+ * Supported: paragraphs, `**bold**`, `*italic*`, inline code, `[links](url)`
1576
+ * (handed to `Linking`), bullet and ordered lists, `#` through `######`
1577
+ * headings, fenced code blocks, blockquotes (which may contain further
1578
+ * blocks), horizontal rules, and backslash escapes.
1579
+ *
1580
+ * Not supported, deliberately: TABLES — GFM's headline feature, but a
1581
+ * phone-width bubble has nowhere to put one, so a table degrades to a
1582
+ * paragraph of its pipe-separated source. Also absent: images (rendered as
1583
+ * their alt text), strikethrough, task lists, footnotes, reference links, raw
1584
+ * HTML, and nested list indentation (nested items flatten into their parent
1585
+ * list).
1586
+ *
1587
+ * Soft line breaks inside a paragraph collapse into spaces, which is what the
1588
+ * browser does for the web widget.
1589
+ */
1590
+
1591
+ interface MarkdownProps {
1592
+ content: string;
1593
+ /** Body text. */
1594
+ color: string;
1595
+ /** Chrome that is not text: the rule, the quote bar, the code outline. */
1596
+ mutedColor: string;
1597
+ linkColor: string;
1598
+ fontSize?: number;
1599
+ fontFamily?: string;
1600
+ }
1601
+ declare const Markdown: FC<MarkdownProps>;
1602
+
1603
+ /**
1604
+ * Node identifiers for integrators and end-to-end tests.
1605
+ *
1606
+ * The web widget tags every meaningful node with `data-ringg="..."` and treats
1607
+ * those names as a contract — integrators select on them, so renaming one is a
1608
+ * breaking change. RN has no attribute namespace, but it has `testID`, which
1609
+ * surfaces to the same audiences (Detox, Maestro, Appium, the native view
1610
+ * hierarchy).
1611
+ *
1612
+ * So the mapping is mechanical and total: `data-ringg="header-title"` becomes
1613
+ * `testID="ringg-header-title"`. The prefix keeps widget nodes from colliding
1614
+ * with the host app's own testIDs, and the suffix stays byte-identical to web
1615
+ * so one selector list documents both platforms.
1616
+ */
1617
+ /** `"header-title"` → `"ringg-header-title"`. */
1618
+ declare const ringgId: (name: string) => string;
1619
+
1620
+ export { type ApiClientConfig, type AudioSessionPort, type BaseComponent, type BlocksActionWire, type BlocksData, type BlocksPayload, type ButtonAction, type ButtonIconConfig, type ButtonItem, type ButtonSize, type ButtonStyle, type ButtonsConfig, type ButtonsData, type ButtonsPayload, CHAT_WIDGET_AFTER_TEXT_MS, CHAT_WIDGET_GRACE_MS, type CalendarBookingData, type CalendarBookingEventPayload, type CalendarBookingPayload, type CalendarSlot, type ChatMessage, type Clock, type ComponentAcknowledgementPayload, type ComponentActionResponse, type ComponentPayload, type ComponentSnapshot, type ComponentStore, type ComponentTheme, type ConfirmationData, type ConfirmationPayload, type ConnectionState, type ControlledRinggWidgetProps, type ControllerPorts, type ConversationStatusPayload, type CustomComponentPayload, WIDGET_DEFAULTS as DEFAULT_CONFIG, DEFAULT_ENVIRONMENT_URLS, DEFAULT_WIDGET_THEME, type DomAction, type DomActionKind, type DomActionLogEntry, type DomActionLogger, type DynamicDataExtension, type EnvironmentMode, type EnvironmentUrls, type ErrorState, type EventBus, type EventLogsConfig, type FeedbackPayload, type FeedbackScreenConfig, type FeedbackStatusPayload, type FlowStep, type FormData, type FormField, type FormFieldOption, type FormFieldType, type FormPayload, GradientFill, type GroupedSlots, type HostAction, type HostActionHandler, type IncomingChatMessage, type IncomingTextStream, type InnerWindowConfig, type InteractiveFlowData, type InteractiveFlowPayload, type LegalDisclaimerConfig, type LiveKitTransport, type LiveKitTransportOptions, MIN_TYPING_INDICATOR_MS, type ManagedRinggWidgetProps, Markdown, type MediaType, type MessageSnapshot, type MessageStore, type MicPermissionPort, type NotificationPlayer, type PendingResponse, type PlayTune, type PortableStyles, type ResolvedWidgetTheme, type RinggApiClient, RinggWidget, type RinggWidgetConfig, type RinggWidgetController, type RinggWidgetProps, type RpcInvocation, type RpcMessage, type SessionPhase, type SessionSnapshot, type SessionStartResult, type SessionStore, type ShellSnapshot, type ShellStore, type SimpleComponentPayload, type SlashCommand, type SlashCommandSnapshot, type SlashCommandStore, type SlashCommandToolType, type Store, type StoreHandle, type TimerHandle, type TranscriptionSegment, type TransportAdapter, type TriggerAlignment, type TriggerPlacement, type TypingSnapshot, type TypingStore, type UrlResolver, type VoiceCallConfig, WIDGET_PRESET_THEMES, type WebcallResponse, type WidgetAlignment, type WidgetEventMap, type WidgetEventName, type WidgetPositionConfig, type WidgetStatusPayload, type WidgetTheme, WidgetThemeProvider, type WidgetVariables, type WidgetViewState, appOrigin, buildPayload, colorToBackground, colorToBorder, colorToSolid, createAudioSession, createCallbackEventBus, createComponentStore, createDomEventBus, createHostActionDispatcher, createLiveKitTransport, createMessageStore, createNativeMicPermission, createNotificationPlayer, createRinggApiClient, createRinggWidgetController, createSessionStore, createShellStore, createSilentNotificationPlayer, createSlashCommandStore, createStaticUrlResolver, createStore, createSystemClock, createTypingStore, defaultUrlResolver, extractColorsFromGradient, formatBlocksAction, formatComponentResponse, formatDynamicData, formatSlashCommand, formatSlotDate, formatSlotTime, getButtonRadius, getContrastingTextColor, getDominantColor, grantedMicPermission, groupSlotsByDate, isBlocks, isButtons, isCalendarBooking, isConfirmation, isForm, isGradient, isInteractiveFlow, isLightColor, mergeComponentTheme, mergeWidgetTheme, resolveButtonRadius, resolveFontFamily, resolveRadius, ringgId, silentNotificationPlayer, solidColor, toSize, toViewStyle, transformRpcToComponent, useRinggComponents, useRinggMessages, useRinggSession, useRinggShell, useRinggSlashCommands, useRinggTyping, useStoreSnapshot, useWidgetTheme };