@ringg/react-native 0.3.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.mjs CHANGED
@@ -14,6 +14,117 @@ var DEFAULT_WIDGET_THEME = {
14
14
  "borderRadius": "16px",
15
15
  "fontFamily": "inherit"
16
16
  };
17
+ var WIDGET_PRESET_THEMES = {
18
+ "light": {
19
+ "primaryColor": "#18181b",
20
+ "primaryTextColor": "#ffffff",
21
+ "backgroundColor": "#ffffff",
22
+ "surfaceColor": "#f4f4f5",
23
+ "textColor": "#18181b",
24
+ "mutedTextColor": "#71717a",
25
+ "borderColor": "#e4e4e7"
26
+ },
27
+ "warm": {
28
+ "primaryColor": "#57534e",
29
+ "primaryTextColor": "#ffffff",
30
+ "backgroundColor": "#fefefe",
31
+ "surfaceColor": "#fafaf9",
32
+ "textColor": "#1c1917",
33
+ "mutedTextColor": "#78716c",
34
+ "borderColor": "#e7e5e4"
35
+ },
36
+ "blue": {
37
+ "primaryColor": "#2563eb",
38
+ "primaryTextColor": "#ffffff",
39
+ "backgroundColor": "#fafcff",
40
+ "surfaceColor": "#f0f7ff",
41
+ "textColor": "#1e3a8a",
42
+ "mutedTextColor": "#64748b",
43
+ "borderColor": "#dbeafe"
44
+ },
45
+ "indigo": {
46
+ "primaryColor": "#6366f1",
47
+ "primaryTextColor": "#ffffff",
48
+ "backgroundColor": "#fbfbff",
49
+ "surfaceColor": "#f5f5ff",
50
+ "textColor": "#312e81",
51
+ "mutedTextColor": "#6b7280",
52
+ "borderColor": "#e0e7ff"
53
+ },
54
+ "green": {
55
+ "primaryColor": "#16a34a",
56
+ "primaryTextColor": "#ffffff",
57
+ "backgroundColor": "#fbfefb",
58
+ "surfaceColor": "#f0fdf4",
59
+ "textColor": "#14532d",
60
+ "mutedTextColor": "#6b7280",
61
+ "borderColor": "#dcfce7"
62
+ },
63
+ "orange": {
64
+ "primaryColor": "#ea580c",
65
+ "primaryTextColor": "#ffffff",
66
+ "backgroundColor": "#fffdfb",
67
+ "surfaceColor": "#fff7ed",
68
+ "textColor": "#7c2d12",
69
+ "mutedTextColor": "#78716c",
70
+ "borderColor": "#ffedd5"
71
+ },
72
+ "dark": {
73
+ "primaryColor": "#fafafa",
74
+ "primaryTextColor": "#18181b",
75
+ "backgroundColor": "#18181b",
76
+ "surfaceColor": "#27272a",
77
+ "textColor": "#fafafa",
78
+ "mutedTextColor": "#a1a1aa",
79
+ "borderColor": "#3f3f46"
80
+ },
81
+ "sunset": {
82
+ "primaryColor": "linear-gradient(135deg, #f97316, #ef4444)",
83
+ "primaryTextColor": "#ffffff",
84
+ "backgroundColor": "#fffbf7",
85
+ "surfaceColor": "#fff3e8",
86
+ "textColor": "#7c2d12",
87
+ "mutedTextColor": "#9a3412",
88
+ "borderColor": "#fed7aa"
89
+ },
90
+ "ocean": {
91
+ "primaryColor": "linear-gradient(135deg, #667eea, #764ba2)",
92
+ "primaryTextColor": "#ffffff",
93
+ "backgroundColor": "#faf9ff",
94
+ "surfaceColor": "#f0edff",
95
+ "textColor": "#312e81",
96
+ "mutedTextColor": "#6366f1",
97
+ "borderColor": "#ddd6fe"
98
+ },
99
+ "emerald": {
100
+ "primaryColor": "linear-gradient(135deg, #10b981, #0ea5e9)",
101
+ "primaryTextColor": "#ffffff",
102
+ "backgroundColor": "#f7fdfb",
103
+ "surfaceColor": "#ecfdf5",
104
+ "textColor": "#064e3b",
105
+ "mutedTextColor": "#047857",
106
+ "borderColor": "#a7f3d0"
107
+ },
108
+ "midnight": {
109
+ "primaryColor": "linear-gradient(135deg, #6366f1, #8b5cf6)",
110
+ "primaryTextColor": "#ffffff",
111
+ "backgroundColor": "#18181b",
112
+ "surfaceColor": "#27272a",
113
+ "textColor": "#fafafa",
114
+ "mutedTextColor": "#a1a1aa",
115
+ "borderColor": "#3f3f46"
116
+ },
117
+ "policybazaar": {
118
+ "primaryColor": "linear-gradient(88deg, #9403fd, #00adfe)",
119
+ "primaryTextColor": "#ffffff",
120
+ "backgroundColor": "#ffffff",
121
+ "surfaceColor": "#f4f2ff",
122
+ "agentBubbleColor": "linear-gradient(107deg, #f0fff9, #f7eeff)",
123
+ "textColor": "#18181b",
124
+ "mutedTextColor": "#64748b",
125
+ "borderColor": "#e6e2ff"
126
+ }
127
+ };
17
128
  var TIMING = {
18
129
  "chatWidgetGraceMs": 800,
19
130
  "chatWidgetAfterTextMs": 120,
@@ -34,6 +145,47 @@ var WIDGET_DEFAULTS = {
34
145
  };
35
146
 
36
147
  // ../core/dist/ports/event-bus.js
148
+ function createDomEventBus() {
149
+ const listeners = /* @__PURE__ */ new Map();
150
+ return {
151
+ emit(event, payload) {
152
+ if (typeof window !== "undefined") {
153
+ window.dispatchEvent(new CustomEvent(event, { detail: payload }));
154
+ }
155
+ },
156
+ on(event, handler) {
157
+ if (typeof window === "undefined")
158
+ return () => {
159
+ };
160
+ const wrapped = ((e) => handler(e.detail));
161
+ let set = listeners.get(event);
162
+ if (!set) {
163
+ set = /* @__PURE__ */ new Set();
164
+ listeners.set(event, set);
165
+ }
166
+ set.add(wrapped);
167
+ window.addEventListener(event, wrapped);
168
+ return () => {
169
+ window.removeEventListener(event, wrapped);
170
+ listeners.get(event)?.delete(wrapped);
171
+ };
172
+ },
173
+ off(event) {
174
+ const remove = (name, handlers) => {
175
+ handlers.forEach((h) => window.removeEventListener(name, h));
176
+ };
177
+ if (event) {
178
+ const handlers = listeners.get(event);
179
+ if (handlers)
180
+ remove(event, handlers);
181
+ listeners.delete(event);
182
+ } else {
183
+ listeners.forEach((handlers, name) => remove(name, handlers));
184
+ listeners.clear();
185
+ }
186
+ }
187
+ };
188
+ }
37
189
  function createCallbackEventBus() {
38
190
  const listeners = /* @__PURE__ */ new Map();
39
191
  return {
@@ -88,6 +240,13 @@ var silentNotificationPlayer = {
88
240
  };
89
241
 
90
242
  // ../core/dist/api/client.js
243
+ function createStaticUrlResolver(urls) {
244
+ return {
245
+ resolve(mode) {
246
+ return urls[mode];
247
+ }
248
+ };
249
+ }
91
250
  function createRinggApiClient(config) {
92
251
  const { backendUrl, livekitUrl } = config.urlResolver.resolve(config.mode);
93
252
  const authHeaders = config.authorization ? { Authorization: config.authorization } : config.xApiKey ? { "X-API-KEY": config.xApiKey } : {};
@@ -212,11 +371,47 @@ function getDominantColor(color) {
212
371
  return color;
213
372
  return extractColorsFromGradient(color)[0] ?? color;
214
373
  }
374
+ function colorToBackground(color) {
375
+ if (isGradient(color)) {
376
+ return { background: color };
377
+ }
378
+ return { backgroundColor: color };
379
+ }
380
+ function colorToSolid(color) {
381
+ return getDominantColor(color);
382
+ }
383
+ function colorToBorder(color) {
384
+ if (isGradient(color)) {
385
+ return {
386
+ borderImage: `${color} 1`,
387
+ borderStyle: "solid"
388
+ };
389
+ }
390
+ return { borderColor: color };
391
+ }
215
392
 
216
393
  // ../core/dist/theme/theme-engine.js
217
394
  function mergeWidgetTheme(theme) {
218
395
  return { ...DEFAULT_WIDGET_THEME, ...theme };
219
396
  }
397
+ function mergeComponentTheme(componentTheme, widgetTheme) {
398
+ const base = widgetTheme ?? DEFAULT_WIDGET_THEME;
399
+ return {
400
+ primaryColor: base.primaryColor,
401
+ primaryTextColor: base.primaryTextColor,
402
+ backgroundColor: base.backgroundColor,
403
+ surfaceColor: base.surfaceColor,
404
+ textColor: base.textColor,
405
+ mutedTextColor: base.mutedTextColor,
406
+ borderColor: base.borderColor,
407
+ errorColor: base.errorColor,
408
+ successColor: base.successColor,
409
+ fontFamily: base.fontFamily,
410
+ borderRadius: base.borderRadius,
411
+ buttonStyle: base.buttonStyle,
412
+ ...componentTheme
413
+ };
414
+ }
220
415
  function getButtonRadius(style) {
221
416
  switch (style) {
222
417
  case "pill":
@@ -228,6 +423,25 @@ function getButtonRadius(style) {
228
423
  return "12px";
229
424
  }
230
425
  }
426
+ function getLuminance(hexColor) {
427
+ const solid = getDominantColor(hexColor);
428
+ const hex = solid.replace("#", "");
429
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
430
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
431
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
432
+ const toLinear = (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
433
+ return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
434
+ }
435
+ function isLightColor(hexColor) {
436
+ try {
437
+ return getLuminance(hexColor) > 0.5;
438
+ } catch {
439
+ return true;
440
+ }
441
+ }
442
+ function getContrastingTextColor(bgColor) {
443
+ return isLightColor(bgColor) ? "#212121" : "#ffffff";
444
+ }
231
445
 
232
446
  // ../core/dist/rpc/formatter.js
233
447
  function transformRpcToComponent(componentName, rpcPayload) {
@@ -411,6 +625,68 @@ function formatDynamicData(payload) {
411
625
  };
412
626
  }
413
627
 
628
+ // ../core/dist/components/helpers.js
629
+ function groupSlotsByDate(slots) {
630
+ const grouped = /* @__PURE__ */ new Map();
631
+ slots.forEach((slot) => {
632
+ const dateKey = new Date(slot.datetime).toISOString().split("T")[0];
633
+ if (!grouped.has(dateKey)) {
634
+ grouped.set(dateKey, []);
635
+ }
636
+ grouped.get(dateKey).push(slot);
637
+ });
638
+ return Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([dateKey, dateSlots]) => ({
639
+ date: dateKey,
640
+ dateLabel: new Date(dateKey).toLocaleDateString("en-US", {
641
+ weekday: "short",
642
+ month: "short",
643
+ day: "numeric"
644
+ }),
645
+ slots: dateSlots.sort((a, b) => new Date(a.datetime).getTime() - new Date(b.datetime).getTime())
646
+ }));
647
+ }
648
+ function formatSlotTime(datetime, timezone) {
649
+ return new Date(datetime).toLocaleTimeString("en-US", {
650
+ hour: "numeric",
651
+ minute: "2-digit",
652
+ hour12: true,
653
+ timeZone: timezone
654
+ });
655
+ }
656
+ function formatSlotDate(datetime, timezone) {
657
+ return new Date(datetime).toLocaleDateString("en-US", {
658
+ weekday: "long",
659
+ month: "long",
660
+ day: "numeric",
661
+ timeZone: timezone
662
+ });
663
+ }
664
+ function buildPayload(template, values) {
665
+ const result = {};
666
+ for (const [key, raw] of Object.entries(template)) {
667
+ result[key] = raw.replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? "");
668
+ }
669
+ return result;
670
+ }
671
+ function isCalendarBooking(payload) {
672
+ return payload.component_type === "calendar_booking";
673
+ }
674
+ function isForm(payload) {
675
+ return payload.component_type === "form";
676
+ }
677
+ function isButtons(payload) {
678
+ return payload.component_type === "buttons";
679
+ }
680
+ function isConfirmation(payload) {
681
+ return payload.component_type === "confirmation";
682
+ }
683
+ function isInteractiveFlow(payload) {
684
+ return payload.component_type === "interactive_flow";
685
+ }
686
+ function isBlocks(payload) {
687
+ return payload.component_type === "blocks";
688
+ }
689
+
414
690
  // ../core/dist/stores/store.js
415
691
  function createStore(build) {
416
692
  const listeners = /* @__PURE__ */ new Set();
@@ -1200,7 +1476,7 @@ var useRinggSlashCommands = (controller) => useStoreSnapshot(controller.slashCom
1200
1476
 
1201
1477
  // src/RinggWidget.tsx
1202
1478
  import { Fragment as Fragment7, useEffect as useEffect16, useMemo as useMemo11, useRef as useRef15, useState as useState16 } from "react";
1203
- import { Animated as Animated15, Image as Image3, KeyboardAvoidingView as KeyboardAvoidingView3, Platform as Platform5, ScrollView as ScrollView5, StyleSheet as StyleSheet24, Text as Text18, View as View21, useWindowDimensions } from "react-native";
1479
+ import { Animated as Animated15, Image as Image3, KeyboardAvoidingView as KeyboardAvoidingView3, Platform as Platform6, ScrollView as ScrollView5, StyleSheet as StyleSheet24, Text as Text18, View as View21, useWindowDimensions } from "react-native";
1204
1480
  import { Minus, X } from "lucide-react-native";
1205
1481
 
1206
1482
  // src/components/action-button.tsx
@@ -1808,7 +2084,7 @@ import { Animated as Animated7, Linking, StyleSheet as StyleSheet12, Text as Tex
1808
2084
  import { Check as Check3, ChevronLeft } from "lucide-react-native";
1809
2085
 
1810
2086
  // src/lib/component-types.ts
1811
- function groupSlotsByDate(slots) {
2087
+ function groupSlotsByDate2(slots) {
1812
2088
  const grouped = /* @__PURE__ */ new Map();
1813
2089
  slots.forEach((slot) => {
1814
2090
  const date = new Date(slot.datetime);
@@ -1827,7 +2103,7 @@ function groupSlotsByDate(slots) {
1827
2103
  };
1828
2104
  });
1829
2105
  }
1830
- function formatSlotTime(datetime, timezone) {
2106
+ function formatSlotTime2(datetime, timezone) {
1831
2107
  const date = new Date(datetime);
1832
2108
  return date.toLocaleTimeString("en-US", {
1833
2109
  hour: "numeric",
@@ -1836,7 +2112,7 @@ function formatSlotTime(datetime, timezone) {
1836
2112
  timeZone: timezone
1837
2113
  });
1838
2114
  }
1839
- function formatSlotDate(datetime, timezone) {
2115
+ function formatSlotDate2(datetime, timezone) {
1840
2116
  const date = new Date(datetime);
1841
2117
  return date.toLocaleDateString("en-US", {
1842
2118
  weekday: "short",
@@ -1845,7 +2121,7 @@ function formatSlotDate(datetime, timezone) {
1845
2121
  timeZone: timezone
1846
2122
  });
1847
2123
  }
1848
- function buildPayload(template, values) {
2124
+ function buildPayload2(template, values) {
1849
2125
  const result = {};
1850
2126
  for (const [key, value] of Object.entries(template)) {
1851
2127
  if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
@@ -1857,13 +2133,13 @@ function buildPayload(template, values) {
1857
2133
  }
1858
2134
  return result;
1859
2135
  }
1860
- function isButtons(payload) {
2136
+ function isButtons2(payload) {
1861
2137
  return payload.component_type === "buttons";
1862
2138
  }
1863
- function isInteractiveFlow(payload) {
2139
+ function isInteractiveFlow2(payload) {
1864
2140
  return payload.component_type === "interactive_flow";
1865
2141
  }
1866
- function isBlocks(payload) {
2142
+ function isBlocks2(payload) {
1867
2143
  return payload.component_type === "blocks";
1868
2144
  }
1869
2145
 
@@ -2075,7 +2351,7 @@ var CalendarStep = ({ payload, isProcessing, onComplete }) => {
2075
2351
  const buttonRadius = getButtonRadius2(theme.buttonStyle);
2076
2352
  const fontFamily = resolveFontFamily(theme.fontFamily);
2077
2353
  const { data } = payload;
2078
- const groupedSlots = useMemo4(() => groupSlotsByDate(data.available_slots), [data.available_slots]);
2354
+ const groupedSlots = useMemo4(() => groupSlotsByDate2(data.available_slots), [data.available_slots]);
2079
2355
  const [selectedDate, setSelectedDate] = useState4(groupedSlots[0]?.date || "");
2080
2356
  const [selectedSlot, setSelectedSlot] = useState4(null);
2081
2357
  const selectedDateGroup = groupedSlots.find((g) => g.date === selectedDate);
@@ -2139,7 +2415,7 @@ var CalendarStep = ({ payload, isProcessing, onComplete }) => {
2139
2415
  {
2140
2416
  color: isSelected ? theme.primaryColor : theme.backgroundColor,
2141
2417
  style: [styles9.slot, { borderRadius: buttonRadius, borderColor: isSelected ? solidColor(theme.primaryColor) : theme.borderColor }, isSelected ? styles9.selectedShadow : null],
2142
- children: /* @__PURE__ */ jsx11(Text5, { style: [styles9.slotLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], numberOfLines: 1, children: formatSlotTime(slot.datetime, data.timezone) })
2418
+ children: /* @__PURE__ */ jsx11(Text5, { style: [styles9.slotLabel, { color: isSelected ? theme.primaryTextColor : theme.textColor, fontFamily }], numberOfLines: 1, children: formatSlotTime2(slot.datetime, data.timezone) })
2143
2419
  }
2144
2420
  ) }) }, slot.id);
2145
2421
  }) })
@@ -2681,8 +2957,8 @@ var EnterFade = ({ translateX = 0, translateY = 0, durationMs = ENTER_DURATION_M
2681
2957
  );
2682
2958
  };
2683
2959
  var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse, onSelectionDisplayed, quickReplyAlign = "end" }) => {
2684
- const isMultiStepFlow = isInteractiveFlow(initialComponent);
2685
- const isFreeButtonGroup = !isMultiStepFlow && isButtons(initialComponent) && initialComponent.data.presentation === "free";
2960
+ const isMultiStepFlow = isInteractiveFlow2(initialComponent);
2961
+ const isFreeButtonGroup = !isMultiStepFlow && isButtons2(initialComponent) && initialComponent.data.presentation === "free";
2686
2962
  const widgetTheme = useWidgetTheme();
2687
2963
  const theme = useMemo6(
2688
2964
  () => mergeTheme({
@@ -2728,7 +3004,7 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2728
3004
  try {
2729
3005
  const isLastStep = currentStepIndex === totalSteps - 1;
2730
3006
  const selectedButtonId = typeof stepData.selected_button === "string" ? stepData.selected_button : null;
2731
- const buttonsComponent = currentComponent !== void 0 && isButtons(currentComponent) ? currentComponent : null;
3007
+ const buttonsComponent = currentComponent !== void 0 && isButtons2(currentComponent) ? currentComponent : null;
2732
3008
  const submittedButton = buttonsComponent && selectedButtonId ? buttonsComponent.data.buttons.find((button) => button.id === selectedButtonId) ?? { id: selectedButtonId, label: selectedButtonId, action: { type: "trigger_component" } } : null;
2733
3009
  const shouldShowSubmittedSelection = buttonsComponent?.data.completionDisplay === "selected_item" && !!submittedButton;
2734
3010
  if (onSendResponse) {
@@ -2767,7 +3043,7 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2767
3043
  for (const [key, value] of Object.entries(newAllData)) {
2768
3044
  stringifiedData[key] = Array.isArray(value) ? JSON.stringify(value) : String(value);
2769
3045
  }
2770
- const apiPayload = buildPayload(on_complete.payload, {
3046
+ const apiPayload = buildPayload2(on_complete.payload, {
2771
3047
  ...stringifiedData,
2772
3048
  all_data: JSON.stringify(newAllData)
2773
3049
  });
@@ -2846,8 +3122,8 @@ var InteractiveFlow = ({ initialComponent, onApiCall, onComplete, onSendResponse
2846
3122
  handleStepComplete({
2847
3123
  slot_id: slotId,
2848
3124
  slot_datetime: slotDatetime,
2849
- slot_time: formatSlotTime(slotDatetime, timezone),
2850
- slot_date: formatSlotDate(slotDatetime, timezone)
3125
+ slot_time: formatSlotTime2(slotDatetime, timezone),
3126
+ slot_date: formatSlotDate2(slotDatetime, timezone)
2851
3127
  });
2852
3128
  }
2853
3129
  }
@@ -4182,7 +4458,7 @@ var FeedbackScreen = ({ callMode, isSubmitting, feedbackScreenConfig, messages,
4182
4458
  transcript.map((message, index) => {
4183
4459
  if (message.componentType && message.componentData) {
4184
4460
  const componentData = message.componentData;
4185
- if (isBlocks(componentData)) {
4461
+ if (isBlocks2(componentData)) {
4186
4462
  return /* @__PURE__ */ jsx19(View14, { testID: ringgId("blocks-widget"), style: styles16.replayed, children: /* @__PURE__ */ jsx19(blocks_step_default, { config: componentData.data, disabled: true, onAction: async () => {
4187
4463
  } }) }, `c_${message.timestamp}__${index}`);
4188
4464
  }
@@ -4878,374 +5154,87 @@ var renderTextWithLinks = (text, links, linkStyle) => {
4878
5154
  return parts;
4879
5155
  };
4880
5156
 
4881
- // src/RinggWidget.tsx
4882
- import { Fragment as Fragment8, jsx as jsx28, jsxs as jsxs20 } from "react/jsx-runtime";
4883
- var PANEL_MAX_WIDTH = 400;
4884
- var PANEL_HEIGHT = 456;
4885
- var PANEL_HEIGHT_RATIO = 0.9;
4886
- var EDGE_INSET = 16;
4887
- var BOTTOM_SAFE_INSET = 24;
4888
- var TRIGGER_SIZE = 52;
4889
- var TRIGGER_RADIUS = 20;
4890
- var DEFAULT_TRIGGER_ICON_SIZE = 24;
4891
- var PANEL_SPRING = { stiffness: 380, damping: 30 };
4892
- var TRIGGER_SPRING = { stiffness: 400, damping: 25 };
4893
- var LEGAL_LINKS = { "Privacy Policy": "https://www.ringg.ai/privacy", "T&C": "https://www.ringg.ai/terms" };
4894
- var LEGAL_TEXT = "By continuing, you agree to our\nPrivacy Policy and T&C.";
4895
- var isFreeButtonsMessage = (message) => {
4896
- const componentData = message?.componentData;
4897
- return !!componentData && isButtons(componentData) && componentData.data.presentation === "free";
5157
+ // src/config/default-urls.ts
5158
+ var DEFAULT_ENVIRONMENT_URLS = {
5159
+ dev: {
5160
+ backendUrl: "https://calling-dev.ringg.ai/ca/api/v0",
5161
+ livekitUrl: "wss://ringg-ai-dev-92tubwpz.livekit.cloud"
5162
+ },
5163
+ stage: {
5164
+ backendUrl: "https://stage-api.ringg.ai/ca/api/v0",
5165
+ livekitUrl: "wss://mercury.webrtc-stage.ringg.ai"
5166
+ },
5167
+ prod: {
5168
+ backendUrl: "https://prod-api.ringg.ai/ca/api/v0",
5169
+ livekitUrl: "wss://mercury.webrtc.ringg.ai"
5170
+ }
4898
5171
  };
4899
- var isAttachableAgentMessage = (message) => !!message && !message.componentType && !message.isSelf && Boolean(message.message.trim());
4900
- var WidgetPanel = ({ controller, room, width, height }) => {
4901
- const theme = useWidgetTheme();
4902
- const { config } = controller;
4903
- const {
4904
- title = WIDGET_DEFAULTS.title,
4905
- description = WIDGET_DEFAULTS.description,
4906
- buttons,
4907
- defaultExpanded = WIDGET_DEFAULTS.defaultExpanded,
4908
- hideTabSelector = WIDGET_DEFAULTS.hideTabSelector,
4909
- bypassStartScreen = WIDGET_DEFAULTS.bypassStartScreen,
4910
- logoUrl,
4911
- logoStyles,
4912
- typingWords,
4913
- legalDisclaimer,
4914
- feedbackScreen,
4915
- voiceCall
4916
- } = config;
4917
- const shell = useRinggShell(controller);
4918
- const session = useRinggSession(controller);
4919
- const { messages } = useRinggMessages(controller);
4920
- const { isTyping: isAgentTyping } = useRinggTyping(controller);
4921
- const { completedFlowIds } = useRinggComponents(controller);
4922
- const { commands } = useRinggSlashCommands(controller);
4923
- const [alertEndCall, setAlertEndCall] = useState16(false);
4924
- const microphone = useMicrophone(room);
4925
- const callMode = shell.callMode;
4926
- const showFeedback = shell.viewState === "feedback";
4927
- const isLoading = session.isLoading;
4928
- const error = session.error;
4929
- const connectionState = session.connectionState;
4930
- const isCalling = connectionState === "connected";
4931
- const isSessionLive = session.isSessionLive;
4932
- const isAudioMode = callMode === "audio";
4933
- const showVoiceAnimation = isAudioMode && isCalling && (voiceCall?.showAnimation ?? WIDGET_DEFAULTS.voiceCallShowAnimation);
4934
- const showVoiceTranscript = !isAudioMode || (voiceCall?.showTranscript ?? WIDGET_DEFAULTS.voiceCallShowTranscript);
4935
- const mergedSlashCommands = useMemo11(() => [...commands], [commands]);
4936
- const sortedMessages = useMemo11(() => [...messages].sort((a, b) => a.timestamp - b.timestamp), [messages]);
4937
- const transcriptRef = useRef15(null);
4938
- const previousMessagesCountRef = useRef15(0);
4939
- useEffect16(() => {
4940
- const newMessageAdded = messages.length > previousMessagesCountRef.current;
4941
- previousMessagesCountRef.current = messages.length;
4942
- transcriptRef.current?.scrollToEnd({ animated: newMessageAdded });
4943
- }, [messages, isAgentTyping]);
4944
- const handleCallStart = (mediaType) => void controller.startCall(mediaType);
4945
- const handleCallEnd = () => {
4946
- setAlertEndCall(false);
4947
- void controller.endCall();
4948
- };
4949
- const handleFeedbackSubmit = (rating, comment) => void controller.submitFeedback(rating, comment);
4950
- const handleFeedbackSkip = () => controller.skipFeedback();
4951
- const handleUserMessageSent = () => controller.typing.showOnUserSend();
4952
- const handleFlowComplete = (componentId) => controller.markFlowComplete(componentId);
4953
- const handleSelectionDisplayed = (componentId, label) => controller.displaySelection(componentId, label);
4954
- const sendComponentResponse = (componentId, responseData) => controller.sendComponentResponse(componentId, responseData);
4955
- const sendSlashCommand = (command) => void controller.sendSlashCommand(command);
4956
- const callComponentApi = controller.callComponentApi;
4957
- const panelRadius = resolveRadius(theme.borderRadius, 16);
4958
- const fontFamily = resolveFontFamily(theme.fontFamily);
4959
- const chrome = /* @__PURE__ */ jsxs20(Fragment8, { children: [
4960
- !defaultExpanded && (isSessionLive || showFeedback) ? /* @__PURE__ */ jsx28(Touchable, { testID: ringgId("minimize-button"), accessibilityLabel: "Minimize widget", onPress: () => controller.minimizeWidget(), style: [styles24.chromeButton, styles24.minimizeButton], children: /* @__PURE__ */ jsx28(Minus, { size: 16, color: solidColor(theme.mutedTextColor) }) }) : null,
4961
- !defaultExpanded ? /* @__PURE__ */ jsx28(
4962
- Touchable,
4963
- {
4964
- testID: ringgId("close-button"),
4965
- accessibilityLabel: isSessionLive ? "End call" : "Close widget",
4966
- onPress: () => isSessionLive ? setAlertEndCall(true) : showFeedback ? handleFeedbackSkip() : controller.minimizeWidget(),
4967
- style: [styles24.chromeButton, styles24.closeButton],
4968
- children: /* @__PURE__ */ jsx28(X, { size: 16, color: solidColor(theme.mutedTextColor) })
4969
- }
4970
- ) : null
4971
- ] });
4972
- const transcript = /* @__PURE__ */ jsxs20(
4973
- ScrollView5,
4974
- {
4975
- testID: ringgId("transcript"),
4976
- ref: transcriptRef,
4977
- style: styles24.transcript,
4978
- contentContainerStyle: [styles24.transcriptList, !showVoiceAnimation && styles24.transcriptTopClearance, !isAudioMode && styles24.transcriptBottomClearance],
4979
- keyboardShouldPersistTaps: "handled",
4980
- showsVerticalScrollIndicator: false,
4981
- children: [
4982
- sortedMessages.map((segment, index) => {
4983
- if (segment.kind === "system") return /* @__PURE__ */ jsx28(system_log_default, { label: segment.message, meta: segment.systemMeta, level: segment.systemLevel }, `sys_${segment.timestamp}_${index}`);
4984
- const previousSegment = sortedMessages[index - 1];
4985
- const nextSegment = sortedMessages[index + 1];
4986
- const attachFreeButtonsToPrevious = isFreeButtonsMessage(segment) && isAttachableAgentMessage(previousSegment);
4987
- const skipStandaloneRender = isAttachableAgentMessage(segment) && isFreeButtonsMessage(nextSegment);
4988
- if (skipStandaloneRender) return null;
4989
- if (attachFreeButtonsToPrevious && previousSegment && segment.componentData) {
4990
- const componentData = segment.componentData;
4991
- const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
4992
- if (isCompletedSelectedItem) return /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: previousSegment }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
4993
- return /* @__PURE__ */ jsxs20(Fragment7, { children: [
4994
- /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: previousSegment }),
4995
- /* @__PURE__ */ jsx28(View21, { testID: ringgId("attached-buttons"), style: styles24.fullWidth, children: /* @__PURE__ */ jsx28(
4996
- interactive_flow_default,
4997
- {
4998
- initialComponent: segment.componentData,
4999
- onApiCall: callComponentApi,
5000
- onComplete: () => handleFlowComplete(componentData.component_id),
5001
- onSendResponse: sendComponentResponse,
5002
- onSelectionDisplayed: handleSelectionDisplayed,
5003
- quickReplyAlign: isAudioMode ? "center" : "end"
5004
- }
5005
- ) })
5006
- ] }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5007
- }
5008
- if (segment.componentType === "blocks" && segment.componentData) {
5009
- const blocksPayload = segment.componentData;
5010
- if (isBlocks(blocksPayload)) {
5011
- const blocksConfig = blocksPayload.data;
5012
- const blockId = blocksPayload.component_id;
5013
- return /* @__PURE__ */ jsx28(View21, { testID: ringgId("blocks-widget"), style: styles24.fullWidth, children: /* @__PURE__ */ jsx28(
5014
- blocks_step_default,
5015
- {
5016
- config: blocksConfig,
5017
- disabled: completedFlowIds.has(blockId),
5018
- onAction: (action) => {
5019
- handleUserMessageSent();
5020
- handleFlowComplete(blockId);
5021
- const selection = typeof action.value === "string" || typeof action.value === "number" ? String(action.value) : action.label;
5022
- if (selection) handleSelectionDisplayed(blockId, selection);
5023
- return controller.sendBlocksAction(blocksConfig.tool_id, blockId, { action_id: action.action_id, value: action.value, values: action.values });
5024
- }
5025
- }
5026
- ) }, `blocks_${blockId}`);
5027
- }
5028
- }
5029
- if (segment.componentType && segment.componentData) {
5030
- const componentData = segment.componentData;
5031
- const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5032
- if (isCompletedSelectedItem) return null;
5033
- return /* @__PURE__ */ jsx28(
5034
- interactive_flow_default,
5035
- {
5036
- initialComponent: segment.componentData,
5037
- onApiCall: callComponentApi,
5038
- onComplete: () => handleFlowComplete(componentData.component_id),
5039
- onSendResponse: sendComponentResponse,
5040
- onSelectionDisplayed: handleSelectionDisplayed,
5041
- quickReplyAlign: isAudioMode ? "center" : "end"
5042
- },
5043
- `flow_${segment.componentData.component_id}_${index}`
5044
- );
5045
- }
5046
- return /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: segment }, `${segment.timestamp}__${index}`);
5047
- }),
5048
- isAgentTyping ? /* @__PURE__ */ jsx28(typing_indicator_default, { words: typingWords }) : null
5049
- ]
5050
- }
5051
- );
5052
- const controls = /* @__PURE__ */ jsx28(
5053
- call_controls_default,
5054
- {
5055
- buttonConfig: buttons,
5056
- callMode,
5057
- isCalling: isSessionLive,
5058
- enabledSlashCommands: mergedSlashCommands,
5059
- micEnabled: microphone?.enabled ?? true,
5060
- onToggleMic: microphone?.toggle,
5061
- onSlashCommand: sendSlashCommand,
5062
- onUserMessage: handleUserMessageSent,
5063
- onCallEnd: handleCallEnd,
5064
- onSendMessage: (message) => controller.sendMessage(message)
5065
- }
5066
- );
5067
- return /* @__PURE__ */ jsxs20(
5068
- View21,
5069
- {
5070
- testID: ringgId("widget-root"),
5071
- style: [styles24.panel, { width, height, borderRadius: panelRadius, backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }],
5072
- children: [
5073
- chrome,
5074
- /* @__PURE__ */ jsx28(
5075
- ConfirmDialog,
5076
- {
5077
- open: alertEndCall,
5078
- title: `Are you sure you want to end ${callMode === "text" ? "chat" : "call"}?`,
5079
- description: "Your current conversation will be closed and cannot be accessed later.",
5080
- confirmLabel: `End ${callMode === "text" ? "Chat" : "Call"}`,
5081
- cancelLabel: "Cancel",
5082
- onConfirm: handleCallEnd,
5083
- onCancel: () => setAlertEndCall(false)
5084
- }
5085
- ),
5086
- showFeedback ? /* @__PURE__ */ jsx28(
5087
- feedback_screen_default,
5088
- {
5089
- callMode,
5090
- isSubmitting: isLoading,
5091
- feedbackScreenConfig: feedbackScreen,
5092
- messages,
5093
- title,
5094
- logoUrl,
5095
- logoStyles,
5096
- onSubmit: handleFeedbackSubmit,
5097
- onSkip: handleFeedbackSkip
5098
- }
5099
- ) : /* @__PURE__ */ jsxs20(View21, { testID: ringgId("widget-body"), style: [styles24.body, isSessionLive ? styles24.bodyLive : styles24.bodyIdle], children: [
5100
- /* @__PURE__ */ jsx28(header_default, { isCalling: isSessionLive, isLoading, title, description, logoUrl, logoStyles }),
5101
- showVoiceAnimation ? /* @__PURE__ */ jsx28(View21, { testID: ringgId("voice-animation-wrapper"), style: [styles24.voiceWrapper, showVoiceTranscript && messages.length > 0 ? styles24.voiceWrapperCompact : styles24.voiceWrapperFull], children: /* @__PURE__ */ jsx28(VoiceAnimationLeaf, { room }) }) : null,
5102
- isSessionLive && !showVoiceAnimation && (messages.length === 0 && !isAgentTyping || !showVoiceTranscript) ? /* @__PURE__ */ jsx28(View21, { testID: ringgId("body-spacer"), style: styles24.bodySpacer }) : null,
5103
- (messages.length > 0 || isAgentTyping) && isSessionLive && showVoiceTranscript ? transcript : null,
5104
- isSessionLive && callMode === "text" ? (
5105
- // Transparent bottom bar: it only positions the composer over the
5106
- // transcript, so the gutters stay clear while messages scroll under.
5107
- /* @__PURE__ */ jsxs20(View21, { testID: ringgId("controls-bar"), style: styles24.controlsBar, children: [
5108
- error.hasError ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null,
5109
- controls
5110
- ] })
5111
- ) : /* @__PURE__ */ jsxs20(Fragment8, { children: [
5112
- controls,
5113
- error.hasError ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null
5114
- ] }),
5115
- !isSessionLive && isLoading ? /* @__PURE__ */ jsxs20(View21, { testID: ringgId("connecting-status"), children: [
5116
- /* @__PURE__ */ jsx28(Text18, { testID: ringgId("connecting-label"), style: [styles24.connecting, { color: theme.mutedTextColor, fontFamily }], children: "Connecting..." }),
5117
- /* @__PURE__ */ jsx28(loading_bar_default, { ariaLabel: callMode === "text" ? "Starting chat" : "Connecting" })
5118
- ] }) : null,
5119
- !isLoading && connectionState === "disconnected" && (!bypassStartScreen || error.hasError) ? hideTabSelector ? /* @__PURE__ */ jsx28(action_button_default, { callMode, buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }) : /* @__PURE__ */ jsxs20(View21, { testID: ringgId("start-actions"), style: styles24.startActions, children: [
5120
- /* @__PURE__ */ jsx28(action_button_default, { dual: true, callMode: "text", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }),
5121
- /* @__PURE__ */ jsx28(action_button_default, { dual: true, callMode: "audio", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart })
5122
- ] }) : null,
5123
- !isSessionLive ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("legal-disclaimer"), style: [styles24.legal, { color: theme.mutedTextColor, fontFamily }], children: legalDisclaimer ? renderTextWithLinks(legalDisclaimer.text, legalDisclaimer.links) : renderTextWithLinks(LEGAL_TEXT, LEGAL_LINKS, { color: solidColor(theme.primaryColor), textDecorationLine: "underline" }) }) : null
5124
- ] })
5125
- ]
5126
- }
5127
- );
5128
- };
5129
- var RinggWidget = ({ controller, room }) => {
5130
- const shell = useRinggShell(controller);
5131
- const { width: screenWidth, height: screenHeight } = useWindowDimensions();
5132
- const { buttons, defaultExpanded = WIDGET_DEFAULTS.defaultExpanded, widgetPosition } = controller.config;
5133
- const isOpen = shell.viewState === "open" || shell.viewState === "feedback";
5134
- const hideTriggerOnExpand = widgetPosition?.hideTriggerOnExpand ?? true;
5135
- const showTrigger = !defaultExpanded && (!hideTriggerOnExpand || !isOpen);
5136
- const panel = usePresence(isOpen, PANEL_SPRING);
5137
- const trigger = usePresence(showTrigger, TRIGGER_SPRING);
5138
- const panelWidth = Math.min(screenWidth - EDGE_INSET * 2, PANEL_MAX_WIDTH);
5139
- const panelHeight = Math.min(screenHeight * PANEL_HEIGHT_RATIO, PANEL_HEIGHT);
5140
- const triggerIconSize = toSize(buttons?.modalTrigger?.icon?.size, DEFAULT_TRIGGER_ICON_SIZE);
5141
- return /* @__PURE__ */ jsx28(WidgetThemeProvider, { theme: controller.theme, children: /* @__PURE__ */ jsxs20(View21, { pointerEvents: "box-none", style: StyleSheet24.absoluteFill, children: [
5142
- panel.mounted ? /* @__PURE__ */ jsx28(KeyboardAvoidingView3, { pointerEvents: "box-none", behavior: Platform5.OS === "ios" ? "padding" : void 0, style: styles24.overlay, children: /* @__PURE__ */ jsx28(
5143
- Animated15.View,
5144
- {
5145
- renderToHardwareTextureAndroid: true,
5146
- shouldRasterizeIOS: true,
5147
- style: [
5148
- styles24.panelAnchor,
5149
- {
5150
- opacity: panel.progress,
5151
- transform: [{ translateY: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }, { scale: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }]
5152
- }
5153
- ],
5154
- children: /* @__PURE__ */ jsx28(WidgetPanel, { controller, room, width: panelWidth, height: panelHeight })
5155
- }
5156
- ) }) : null,
5157
- trigger.mounted ? /* @__PURE__ */ jsx28(WidgetTrigger, { controller, iconSize: triggerIconSize, progress: trigger.progress }) : null
5158
- ] }) });
5159
- };
5160
- var WidgetTrigger = ({ controller, iconSize, progress }) => {
5161
- const theme = useWidgetTheme();
5162
- const { buttons } = controller.config;
5163
- return (
5164
- // Web pops the trigger with a spring and fades it out again when the panel
5165
- // takes over; both ends of that are visible on every open/close cycle.
5166
- /* @__PURE__ */ jsx28(Animated15.View, { style: [styles24.trigger, { opacity: progress, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }] }], children: /* @__PURE__ */ jsx28(Touchable, { testID: ringgId("trigger-button"), accessibilityLabel: "Open call widget", onPress: () => controller.handleTriggerClick(), children: /* @__PURE__ */ jsx28(GradientFill, { color: theme.primaryColor, style: [styles24.triggerSurface, toViewStyle(buttons?.modalTrigger?.styles)], children: buttons?.modalTrigger?.icon?.url ? /* @__PURE__ */ jsx28(
5167
- Image3,
5168
- {
5169
- testID: ringgId("trigger-icon"),
5170
- accessibilityLabel: "Widget trigger",
5171
- source: { uri: buttons.modalTrigger.icon.url },
5172
- resizeMode: "contain",
5173
- style: { width: iconSize, height: iconSize }
5174
- }
5175
- ) : /* @__PURE__ */ jsx28(PhoneCallRingIcon, { testID: ringgId("trigger-icon"), size: iconSize, fill: solidColor(theme.primaryTextColor) }) }) }) })
5176
- );
5177
- };
5178
- var styles24 = StyleSheet24.create({
5179
- overlay: { ...StyleSheet24.absoluteFill, justifyContent: "flex-end", alignItems: "flex-end" },
5180
- panelAnchor: { marginHorizontal: EDGE_INSET, marginTop: EDGE_INSET, marginBottom: BOTTOM_SAFE_INSET },
5181
- panel: {
5182
- borderWidth: StyleSheet24.hairlineWidth,
5183
- // `overflow: hidden` is what keeps the transcript, the header bar and the
5184
- // rounded corners agreeing with each other.
5185
- overflow: "hidden",
5186
- // Web stacks three shadows at 1-2% alpha — the panel sits just off the page
5187
- // rather than floating above it. 12% read as a hard slab, and on Android
5188
- // `elevation` is the only one of these that applies, so it was the entire
5189
- // effect instead of a supporting layer.
5190
- shadowColor: "#000000",
5191
- shadowOpacity: 0.05,
5192
- shadowRadius: 20,
5193
- shadowOffset: { width: 0, height: 8 },
5194
- elevation: 4
5195
- },
5196
- trigger: { position: "absolute", right: EDGE_INSET, bottom: BOTTOM_SAFE_INSET },
5197
- triggerSurface: { width: TRIGGER_SIZE, height: TRIGGER_SIZE, borderRadius: TRIGGER_RADIUS, alignItems: "center", justifyContent: "center" },
5198
- chromeButton: { position: "absolute", top: 10, zIndex: 20, width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
5199
- minimizeButton: { right: 42 },
5200
- closeButton: { right: 10 },
5201
- body: { flex: 1 },
5202
- bodyLive: { paddingHorizontal: 20, paddingVertical: 8, gap: 10 },
5203
- bodyIdle: { padding: 24, gap: 12 },
5204
- bodySpacer: { flex: 1, marginTop: 56 },
5205
- voiceWrapper: { width: "100%", alignItems: "stretch", justifyContent: "center", marginTop: 56 },
5206
- voiceWrapperCompact: { height: 56, flexShrink: 0 },
5207
- voiceWrapperFull: { flex: 1, minHeight: 0 },
5208
- transcript: { flex: 1 },
5209
- // `justifyContent: flex-end` is web's `mt-auto`: a short conversation sits at
5210
- // the bottom of the panel rather than floating at the top.
5211
- transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12 },
5212
- transcriptTopClearance: { paddingTop: 64 },
5213
- transcriptBottomClearance: { paddingBottom: 80 },
5214
- fullWidth: { width: "100%" },
5215
- controlsBar: { position: "absolute", zIndex: 10, bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingVertical: 12 },
5216
- error: { textAlign: "center", fontSize: 14, marginBottom: 8 },
5217
- connecting: { fontSize: 14, textAlign: "center" },
5218
- startActions: { flexDirection: "row", alignItems: "center", gap: 8, width: "100%" },
5219
- legal: { fontSize: 12, lineHeight: 20, textAlign: "center" }
5220
- });
5221
-
5222
- // src/transport/livekit-transport.ts
5223
- import { registerGlobals } from "@livekit/react-native";
5224
- import {
5225
- ConnectionState as LKConnectionState,
5226
- Room,
5227
- RoomEvent as RoomEvent3
5228
- } from "livekit-client";
5229
-
5230
- // src/platform/audio-session.ts
5231
- import { AndroidAudioTypePresets, AudioSession } from "@livekit/react-native";
5232
- var PREFERRED_OUTPUTS = ["bluetooth", "headset", "speaker", "earpiece"];
5233
- var createAudioSession = () => {
5234
- let active = false;
5235
- const start = async () => {
5236
- if (active) return;
5237
- active = true;
5238
- try {
5239
- await AudioSession.configureAudio({
5240
- android: {
5241
- audioTypeOptions: AndroidAudioTypePresets.communication,
5242
- preferredOutputList: [...PREFERRED_OUTPUTS]
5243
- },
5244
- ios: { defaultOutput: "speaker" }
5245
- });
5246
- await AudioSession.startAudioSession();
5247
- } catch {
5248
- }
5172
+ var defaultUrlResolver = createStaticUrlResolver(DEFAULT_ENVIRONMENT_URLS);
5173
+
5174
+ // src/platform/mic-permission.ts
5175
+ import { PermissionsAndroid, Platform as Platform5 } from "react-native";
5176
+ import { mediaDevices } from "@livekit/react-native-webrtc";
5177
+ var stopTracks = (stream) => {
5178
+ stream.getTracks().forEach((track) => track.stop());
5179
+ };
5180
+ var probeMicrophone = async () => {
5181
+ try {
5182
+ const stream = await mediaDevices.getUserMedia({ audio: true });
5183
+ stopTracks(stream);
5184
+ return true;
5185
+ } catch {
5186
+ return false;
5187
+ }
5188
+ };
5189
+ var ANDROID_RATIONALE = {
5190
+ title: "Microphone access",
5191
+ message: "Voice calls need the microphone to hear you.",
5192
+ buttonPositive: "Allow",
5193
+ buttonNegative: "Not now"
5194
+ };
5195
+ var createNativeMicPermission = () => ({
5196
+ async isGranted() {
5197
+ if (Platform5.OS === "android") {
5198
+ return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
5199
+ }
5200
+ return probeMicrophone();
5201
+ },
5202
+ async request() {
5203
+ if (Platform5.OS === "android") {
5204
+ const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, ANDROID_RATIONALE);
5205
+ return result === PermissionsAndroid.RESULTS.GRANTED;
5206
+ }
5207
+ return probeMicrophone();
5208
+ }
5209
+ });
5210
+
5211
+ // src/transport/livekit-transport.ts
5212
+ import { registerGlobals } from "@livekit/react-native";
5213
+ import {
5214
+ ConnectionState as LKConnectionState,
5215
+ Room,
5216
+ RoomEvent as RoomEvent3
5217
+ } from "livekit-client";
5218
+
5219
+ // src/platform/audio-session.ts
5220
+ import { AndroidAudioTypePresets, AudioSession } from "@livekit/react-native";
5221
+ var PREFERRED_OUTPUTS = ["bluetooth", "headset", "speaker", "earpiece"];
5222
+ var createAudioSession = () => {
5223
+ let active = false;
5224
+ const start = async () => {
5225
+ if (active) return;
5226
+ active = true;
5227
+ try {
5228
+ await AudioSession.configureAudio({
5229
+ android: {
5230
+ audioTypeOptions: AndroidAudioTypePresets.communication,
5231
+ preferredOutputList: [...PREFERRED_OUTPUTS]
5232
+ },
5233
+ ios: { defaultOutput: "speaker" }
5234
+ });
5235
+ await AudioSession.startAudioSession();
5236
+ } catch {
5237
+ }
5249
5238
  };
5250
5239
  const stop = async () => {
5251
5240
  if (!active) return;
@@ -5552,60 +5541,390 @@ var createLiveKitTransport = (options = {}) => {
5552
5541
  room.off(RoomEvent3.MediaDevicesError, listener);
5553
5542
  };
5554
5543
  }
5555
- };
5556
- const dispose = () => {
5557
- room.off(RoomEvent3.DataReceived, handleDataReceived);
5558
- room.off(RoomEvent3.Disconnected, handleRoomDisconnected);
5559
- for (const topic of Array.from(registeredTextStreamTopics)) {
5560
- room.unregisterTextStreamHandler(topic);
5544
+ };
5545
+ const dispose = () => {
5546
+ room.off(RoomEvent3.DataReceived, handleDataReceived);
5547
+ room.off(RoomEvent3.Disconnected, handleRoomDisconnected);
5548
+ for (const topic of Array.from(registeredTextStreamTopics)) {
5549
+ room.unregisterTextStreamHandler(topic);
5550
+ }
5551
+ registeredTextStreamTopics.clear();
5552
+ for (const method of Array.from(registeredRpcMethods)) {
5553
+ room.unregisterRpcMethod(method);
5554
+ }
5555
+ registeredRpcMethods.clear();
5556
+ chatHandlers.clear();
5557
+ firstSeenAt.clear();
5558
+ void room.disconnect().finally(() => audioSession?.stop());
5559
+ };
5560
+ return { transport, room, dispose };
5561
+ };
5562
+
5563
+ // src/RinggWidget.tsx
5564
+ import { Fragment as Fragment8, jsx as jsx28, jsxs as jsxs20 } from "react/jsx-runtime";
5565
+ var PANEL_MAX_WIDTH = 400;
5566
+ var PANEL_HEIGHT = 456;
5567
+ var PANEL_HEIGHT_RATIO = 0.9;
5568
+ var EDGE_INSET = 16;
5569
+ var BOTTOM_SAFE_INSET = 24;
5570
+ var TRIGGER_SIZE = 52;
5571
+ var TRIGGER_RADIUS = 20;
5572
+ var DEFAULT_TRIGGER_ICON_SIZE = 24;
5573
+ var PANEL_SPRING = { stiffness: 380, damping: 30 };
5574
+ var TRIGGER_SPRING = { stiffness: 400, damping: 25 };
5575
+ var LEGAL_LINKS = { "Privacy Policy": "https://www.ringg.ai/privacy", "T&C": "https://www.ringg.ai/terms" };
5576
+ var LEGAL_TEXT = "By continuing, you agree to our\nPrivacy Policy and T&C.";
5577
+ var isFreeButtonsMessage = (message) => {
5578
+ const componentData = message?.componentData;
5579
+ return !!componentData && isButtons2(componentData) && componentData.data.presentation === "free";
5580
+ };
5581
+ var isAttachableAgentMessage = (message) => !!message && !message.componentType && !message.isSelf && Boolean(message.message.trim());
5582
+ var WidgetPanel = ({ controller, room, width, height }) => {
5583
+ const theme = useWidgetTheme();
5584
+ const { config } = controller;
5585
+ const {
5586
+ title = WIDGET_DEFAULTS.title,
5587
+ description = WIDGET_DEFAULTS.description,
5588
+ buttons,
5589
+ defaultExpanded = WIDGET_DEFAULTS.defaultExpanded,
5590
+ hideTabSelector = WIDGET_DEFAULTS.hideTabSelector,
5591
+ bypassStartScreen = WIDGET_DEFAULTS.bypassStartScreen,
5592
+ logoUrl,
5593
+ logoStyles,
5594
+ typingWords,
5595
+ legalDisclaimer,
5596
+ feedbackScreen,
5597
+ voiceCall
5598
+ } = config;
5599
+ const shell = useRinggShell(controller);
5600
+ const session = useRinggSession(controller);
5601
+ const { messages } = useRinggMessages(controller);
5602
+ const { isTyping: isAgentTyping } = useRinggTyping(controller);
5603
+ const { completedFlowIds } = useRinggComponents(controller);
5604
+ const { commands } = useRinggSlashCommands(controller);
5605
+ const [alertEndCall, setAlertEndCall] = useState16(false);
5606
+ const microphone = useMicrophone(room);
5607
+ const callMode = shell.callMode;
5608
+ const showFeedback = shell.viewState === "feedback";
5609
+ const isLoading = session.isLoading;
5610
+ const error = session.error;
5611
+ const connectionState = session.connectionState;
5612
+ const isCalling = connectionState === "connected";
5613
+ const isSessionLive = session.isSessionLive;
5614
+ const isAudioMode = callMode === "audio";
5615
+ const showVoiceAnimation = isAudioMode && isCalling && (voiceCall?.showAnimation ?? WIDGET_DEFAULTS.voiceCallShowAnimation);
5616
+ const showVoiceTranscript = !isAudioMode || (voiceCall?.showTranscript ?? WIDGET_DEFAULTS.voiceCallShowTranscript);
5617
+ const mergedSlashCommands = useMemo11(() => [...commands], [commands]);
5618
+ const sortedMessages = useMemo11(() => [...messages].sort((a, b) => a.timestamp - b.timestamp), [messages]);
5619
+ const transcriptRef = useRef15(null);
5620
+ const previousMessagesCountRef = useRef15(0);
5621
+ useEffect16(() => {
5622
+ const newMessageAdded = messages.length > previousMessagesCountRef.current;
5623
+ previousMessagesCountRef.current = messages.length;
5624
+ transcriptRef.current?.scrollToEnd({ animated: newMessageAdded });
5625
+ }, [messages, isAgentTyping]);
5626
+ const handleCallStart = (mediaType) => void controller.startCall(mediaType);
5627
+ const handleCallEnd = () => {
5628
+ setAlertEndCall(false);
5629
+ void controller.endCall();
5630
+ };
5631
+ const handleFeedbackSubmit = (rating, comment) => void controller.submitFeedback(rating, comment);
5632
+ const handleFeedbackSkip = () => controller.skipFeedback();
5633
+ const handleUserMessageSent = () => controller.typing.showOnUserSend();
5634
+ const handleFlowComplete = (componentId) => controller.markFlowComplete(componentId);
5635
+ const handleSelectionDisplayed = (componentId, label) => controller.displaySelection(componentId, label);
5636
+ const sendComponentResponse = (componentId, responseData) => controller.sendComponentResponse(componentId, responseData);
5637
+ const sendSlashCommand = (command) => void controller.sendSlashCommand(command);
5638
+ const callComponentApi = controller.callComponentApi;
5639
+ const panelRadius = resolveRadius(theme.borderRadius, 16);
5640
+ const fontFamily = resolveFontFamily(theme.fontFamily);
5641
+ const chrome = /* @__PURE__ */ jsxs20(Fragment8, { children: [
5642
+ !defaultExpanded && (isSessionLive || showFeedback) ? /* @__PURE__ */ jsx28(Touchable, { testID: ringgId("minimize-button"), accessibilityLabel: "Minimize widget", onPress: () => controller.minimizeWidget(), style: [styles24.chromeButton, styles24.minimizeButton], children: /* @__PURE__ */ jsx28(Minus, { size: 16, color: solidColor(theme.mutedTextColor) }) }) : null,
5643
+ !defaultExpanded ? /* @__PURE__ */ jsx28(
5644
+ Touchable,
5645
+ {
5646
+ testID: ringgId("close-button"),
5647
+ accessibilityLabel: isSessionLive ? "End call" : "Close widget",
5648
+ onPress: () => isSessionLive ? setAlertEndCall(true) : showFeedback ? handleFeedbackSkip() : controller.minimizeWidget(),
5649
+ style: [styles24.chromeButton, styles24.closeButton],
5650
+ children: /* @__PURE__ */ jsx28(X, { size: 16, color: solidColor(theme.mutedTextColor) })
5651
+ }
5652
+ ) : null
5653
+ ] });
5654
+ const transcript = /* @__PURE__ */ jsxs20(
5655
+ ScrollView5,
5656
+ {
5657
+ testID: ringgId("transcript"),
5658
+ ref: transcriptRef,
5659
+ style: styles24.transcript,
5660
+ contentContainerStyle: [styles24.transcriptList, !showVoiceAnimation && styles24.transcriptTopClearance, !isAudioMode && styles24.transcriptBottomClearance],
5661
+ keyboardShouldPersistTaps: "handled",
5662
+ showsVerticalScrollIndicator: false,
5663
+ children: [
5664
+ sortedMessages.map((segment, index) => {
5665
+ if (segment.kind === "system") return /* @__PURE__ */ jsx28(system_log_default, { label: segment.message, meta: segment.systemMeta, level: segment.systemLevel }, `sys_${segment.timestamp}_${index}`);
5666
+ const previousSegment = sortedMessages[index - 1];
5667
+ const nextSegment = sortedMessages[index + 1];
5668
+ const attachFreeButtonsToPrevious = isFreeButtonsMessage(segment) && isAttachableAgentMessage(previousSegment);
5669
+ const skipStandaloneRender = isAttachableAgentMessage(segment) && isFreeButtonsMessage(nextSegment);
5670
+ if (skipStandaloneRender) return null;
5671
+ if (attachFreeButtonsToPrevious && previousSegment && segment.componentData) {
5672
+ const componentData = segment.componentData;
5673
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5674
+ if (isCompletedSelectedItem) return /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: previousSegment }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5675
+ return /* @__PURE__ */ jsxs20(Fragment7, { children: [
5676
+ /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: previousSegment }),
5677
+ /* @__PURE__ */ jsx28(View21, { testID: ringgId("attached-buttons"), style: styles24.fullWidth, children: /* @__PURE__ */ jsx28(
5678
+ interactive_flow_default,
5679
+ {
5680
+ initialComponent: segment.componentData,
5681
+ onApiCall: callComponentApi,
5682
+ onComplete: () => handleFlowComplete(componentData.component_id),
5683
+ onSendResponse: sendComponentResponse,
5684
+ onSelectionDisplayed: handleSelectionDisplayed,
5685
+ quickReplyAlign: isAudioMode ? "center" : "end"
5686
+ }
5687
+ ) })
5688
+ ] }, `message_with_buttons_${segment.componentData.component_id}_${index}`);
5689
+ }
5690
+ if (segment.componentType === "blocks" && segment.componentData) {
5691
+ const blocksPayload = segment.componentData;
5692
+ if (isBlocks2(blocksPayload)) {
5693
+ const blocksConfig = blocksPayload.data;
5694
+ const blockId = blocksPayload.component_id;
5695
+ return /* @__PURE__ */ jsx28(View21, { testID: ringgId("blocks-widget"), style: styles24.fullWidth, children: /* @__PURE__ */ jsx28(
5696
+ blocks_step_default,
5697
+ {
5698
+ config: blocksConfig,
5699
+ disabled: completedFlowIds.has(blockId),
5700
+ onAction: (action) => {
5701
+ handleUserMessageSent();
5702
+ handleFlowComplete(blockId);
5703
+ const selection = typeof action.value === "string" || typeof action.value === "number" ? String(action.value) : action.label;
5704
+ if (selection) handleSelectionDisplayed(blockId, selection);
5705
+ return controller.sendBlocksAction(blocksConfig.tool_id, blockId, { action_id: action.action_id, value: action.value, values: action.values });
5706
+ }
5707
+ }
5708
+ ) }, `blocks_${blockId}`);
5709
+ }
5710
+ }
5711
+ if (segment.componentType && segment.componentData) {
5712
+ const componentData = segment.componentData;
5713
+ const isCompletedSelectedItem = componentData.component_type === "buttons" && componentData.data.completionDisplay === "selected_item" && completedFlowIds.has(componentData.component_id);
5714
+ if (isCompletedSelectedItem) return null;
5715
+ return /* @__PURE__ */ jsx28(
5716
+ interactive_flow_default,
5717
+ {
5718
+ initialComponent: segment.componentData,
5719
+ onApiCall: callComponentApi,
5720
+ onComplete: () => handleFlowComplete(componentData.component_id),
5721
+ onSendResponse: sendComponentResponse,
5722
+ onSelectionDisplayed: handleSelectionDisplayed,
5723
+ quickReplyAlign: isAudioMode ? "center" : "end"
5724
+ },
5725
+ `flow_${segment.componentData.component_id}_${index}`
5726
+ );
5727
+ }
5728
+ return /* @__PURE__ */ jsx28(message_item_default, { itemIndex: index, message: segment }, `${segment.timestamp}__${index}`);
5729
+ }),
5730
+ isAgentTyping ? /* @__PURE__ */ jsx28(typing_indicator_default, { words: typingWords }) : null
5731
+ ]
5732
+ }
5733
+ );
5734
+ const controls = /* @__PURE__ */ jsx28(
5735
+ call_controls_default,
5736
+ {
5737
+ buttonConfig: buttons,
5738
+ callMode,
5739
+ isCalling: isSessionLive,
5740
+ enabledSlashCommands: mergedSlashCommands,
5741
+ micEnabled: microphone?.enabled ?? true,
5742
+ onToggleMic: microphone?.toggle,
5743
+ onSlashCommand: sendSlashCommand,
5744
+ onUserMessage: handleUserMessageSent,
5745
+ onCallEnd: handleCallEnd,
5746
+ onSendMessage: (message) => controller.sendMessage(message)
5561
5747
  }
5562
- registeredTextStreamTopics.clear();
5563
- for (const method of Array.from(registeredRpcMethods)) {
5564
- room.unregisterRpcMethod(method);
5748
+ );
5749
+ return /* @__PURE__ */ jsxs20(
5750
+ View21,
5751
+ {
5752
+ testID: ringgId("widget-root"),
5753
+ style: [styles24.panel, { width, height, borderRadius: panelRadius, backgroundColor: solidColor(theme.backgroundColor), borderColor: solidColor(theme.borderColor) }],
5754
+ children: [
5755
+ chrome,
5756
+ /* @__PURE__ */ jsx28(
5757
+ ConfirmDialog,
5758
+ {
5759
+ open: alertEndCall,
5760
+ title: `Are you sure you want to end ${callMode === "text" ? "chat" : "call"}?`,
5761
+ description: "Your current conversation will be closed and cannot be accessed later.",
5762
+ confirmLabel: `End ${callMode === "text" ? "Chat" : "Call"}`,
5763
+ cancelLabel: "Cancel",
5764
+ onConfirm: handleCallEnd,
5765
+ onCancel: () => setAlertEndCall(false)
5766
+ }
5767
+ ),
5768
+ showFeedback ? /* @__PURE__ */ jsx28(
5769
+ feedback_screen_default,
5770
+ {
5771
+ callMode,
5772
+ isSubmitting: isLoading,
5773
+ feedbackScreenConfig: feedbackScreen,
5774
+ messages,
5775
+ title,
5776
+ logoUrl,
5777
+ logoStyles,
5778
+ onSubmit: handleFeedbackSubmit,
5779
+ onSkip: handleFeedbackSkip
5780
+ }
5781
+ ) : /* @__PURE__ */ jsxs20(View21, { testID: ringgId("widget-body"), style: [styles24.body, isSessionLive ? styles24.bodyLive : styles24.bodyIdle], children: [
5782
+ /* @__PURE__ */ jsx28(header_default, { isCalling: isSessionLive, isLoading, title, description, logoUrl, logoStyles }),
5783
+ showVoiceAnimation ? /* @__PURE__ */ jsx28(View21, { testID: ringgId("voice-animation-wrapper"), style: [styles24.voiceWrapper, showVoiceTranscript && messages.length > 0 ? styles24.voiceWrapperCompact : styles24.voiceWrapperFull], children: /* @__PURE__ */ jsx28(VoiceAnimationLeaf, { room }) }) : null,
5784
+ isSessionLive && !showVoiceAnimation && (messages.length === 0 && !isAgentTyping || !showVoiceTranscript) ? /* @__PURE__ */ jsx28(View21, { testID: ringgId("body-spacer"), style: styles24.bodySpacer }) : null,
5785
+ (messages.length > 0 || isAgentTyping) && isSessionLive && showVoiceTranscript ? transcript : null,
5786
+ isSessionLive && callMode === "text" ? (
5787
+ // Transparent bottom bar: it only positions the composer over the
5788
+ // transcript, so the gutters stay clear while messages scroll under.
5789
+ /* @__PURE__ */ jsxs20(View21, { testID: ringgId("controls-bar"), style: styles24.controlsBar, children: [
5790
+ error.hasError ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null,
5791
+ controls
5792
+ ] })
5793
+ ) : /* @__PURE__ */ jsxs20(Fragment8, { children: [
5794
+ controls,
5795
+ error.hasError ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("error-message"), style: [styles24.error, { color: solidColor(theme.errorColor), fontFamily }], children: error.message }) : null
5796
+ ] }),
5797
+ !isSessionLive && isLoading ? /* @__PURE__ */ jsxs20(View21, { testID: ringgId("connecting-status"), children: [
5798
+ /* @__PURE__ */ jsx28(Text18, { testID: ringgId("connecting-label"), style: [styles24.connecting, { color: theme.mutedTextColor, fontFamily }], children: "Connecting..." }),
5799
+ /* @__PURE__ */ jsx28(loading_bar_default, { ariaLabel: callMode === "text" ? "Starting chat" : "Connecting" })
5800
+ ] }) : null,
5801
+ !isLoading && connectionState === "disconnected" && (!bypassStartScreen || error.hasError) ? hideTabSelector ? /* @__PURE__ */ jsx28(action_button_default, { callMode, buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }) : /* @__PURE__ */ jsxs20(View21, { testID: ringgId("start-actions"), style: styles24.startActions, children: [
5802
+ /* @__PURE__ */ jsx28(action_button_default, { dual: true, callMode: "text", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart }),
5803
+ /* @__PURE__ */ jsx28(action_button_default, { dual: true, callMode: "audio", buttonConfig: buttons, isCalling, isLoading, onCallTrigger: handleCallStart })
5804
+ ] }) : null,
5805
+ !isSessionLive ? /* @__PURE__ */ jsx28(Text18, { testID: ringgId("legal-disclaimer"), style: [styles24.legal, { color: theme.mutedTextColor, fontFamily }], children: legalDisclaimer ? renderTextWithLinks(legalDisclaimer.text, legalDisclaimer.links) : renderTextWithLinks(LEGAL_TEXT, LEGAL_LINKS, { color: solidColor(theme.primaryColor), textDecorationLine: "underline" }) }) : null
5806
+ ] })
5807
+ ]
5565
5808
  }
5566
- registeredRpcMethods.clear();
5567
- chatHandlers.clear();
5568
- firstSeenAt.clear();
5569
- void room.disconnect().finally(() => audioSession?.stop());
5570
- };
5571
- return { transport, room, dispose };
5809
+ );
5572
5810
  };
5573
-
5574
- // src/platform/mic-permission.ts
5575
- import { PermissionsAndroid, Platform as Platform6 } from "react-native";
5576
- import { mediaDevices } from "@livekit/react-native-webrtc";
5577
- var stopTracks = (stream) => {
5578
- stream.getTracks().forEach((track) => track.stop());
5811
+ var ControlledRinggWidget = ({ controller, room }) => {
5812
+ const shell = useRinggShell(controller);
5813
+ const { width: screenWidth, height: screenHeight } = useWindowDimensions();
5814
+ const { buttons, defaultExpanded = WIDGET_DEFAULTS.defaultExpanded, widgetPosition } = controller.config;
5815
+ const isOpen = shell.viewState === "open" || shell.viewState === "feedback";
5816
+ const hideTriggerOnExpand = widgetPosition?.hideTriggerOnExpand ?? true;
5817
+ const showTrigger = !defaultExpanded && (!hideTriggerOnExpand || !isOpen);
5818
+ const panel = usePresence(isOpen, PANEL_SPRING);
5819
+ const trigger = usePresence(showTrigger, TRIGGER_SPRING);
5820
+ const panelWidth = Math.min(screenWidth - EDGE_INSET * 2, PANEL_MAX_WIDTH);
5821
+ const panelHeight = Math.min(screenHeight * PANEL_HEIGHT_RATIO, PANEL_HEIGHT);
5822
+ const triggerIconSize = toSize(buttons?.modalTrigger?.icon?.size, DEFAULT_TRIGGER_ICON_SIZE);
5823
+ return /* @__PURE__ */ jsx28(WidgetThemeProvider, { theme: controller.theme, children: /* @__PURE__ */ jsxs20(View21, { pointerEvents: "box-none", style: StyleSheet24.absoluteFill, children: [
5824
+ panel.mounted ? /* @__PURE__ */ jsx28(KeyboardAvoidingView3, { pointerEvents: "box-none", behavior: Platform6.OS === "ios" ? "padding" : void 0, style: styles24.overlay, children: /* @__PURE__ */ jsx28(
5825
+ Animated15.View,
5826
+ {
5827
+ renderToHardwareTextureAndroid: true,
5828
+ shouldRasterizeIOS: true,
5829
+ style: [
5830
+ styles24.panelAnchor,
5831
+ {
5832
+ opacity: panel.progress,
5833
+ transform: [{ translateY: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [24, 0] }) }, { scale: panel.progress.interpolate({ inputRange: [0, 1], outputRange: [0.96, 1] }) }]
5834
+ }
5835
+ ],
5836
+ children: /* @__PURE__ */ jsx28(WidgetPanel, { controller, room, width: panelWidth, height: panelHeight })
5837
+ }
5838
+ ) }) : null,
5839
+ trigger.mounted ? /* @__PURE__ */ jsx28(WidgetTrigger, { controller, iconSize: triggerIconSize, progress: trigger.progress }) : null
5840
+ ] }) });
5579
5841
  };
5580
- var probeMicrophone = async () => {
5581
- try {
5582
- const stream = await mediaDevices.getUserMedia({ audio: true });
5583
- stopTracks(stream);
5584
- return true;
5585
- } catch {
5586
- return false;
5587
- }
5842
+ var WidgetTrigger = ({ controller, iconSize, progress }) => {
5843
+ const theme = useWidgetTheme();
5844
+ const { buttons } = controller.config;
5845
+ return (
5846
+ // Web pops the trigger with a spring and fades it out again when the panel
5847
+ // takes over; both ends of that are visible on every open/close cycle.
5848
+ /* @__PURE__ */ jsx28(Animated15.View, { style: [styles24.trigger, { opacity: progress, transform: [{ scale: progress.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1] }) }] }], children: /* @__PURE__ */ jsx28(Touchable, { testID: ringgId("trigger-button"), accessibilityLabel: "Open call widget", onPress: () => controller.handleTriggerClick(), children: /* @__PURE__ */ jsx28(GradientFill, { color: theme.primaryColor, style: [styles24.triggerSurface, toViewStyle(buttons?.modalTrigger?.styles)], children: buttons?.modalTrigger?.icon?.url ? /* @__PURE__ */ jsx28(
5849
+ Image3,
5850
+ {
5851
+ testID: ringgId("trigger-icon"),
5852
+ accessibilityLabel: "Widget trigger",
5853
+ source: { uri: buttons.modalTrigger.icon.url },
5854
+ resizeMode: "contain",
5855
+ style: { width: iconSize, height: iconSize }
5856
+ }
5857
+ ) : /* @__PURE__ */ jsx28(PhoneCallRingIcon, { testID: ringgId("trigger-icon"), size: iconSize, fill: solidColor(theme.primaryTextColor) }) }) }) })
5858
+ );
5588
5859
  };
5589
- var ANDROID_RATIONALE = {
5590
- title: "Microphone access",
5591
- message: "Voice calls need the microphone to hear you.",
5592
- buttonPositive: "Allow",
5593
- buttonNegative: "Not now"
5860
+ var ManagedRinggWidget = ({ config, ports, onReady }) => {
5861
+ const stack = useMemo11(() => {
5862
+ const livekit = ports?.transport ? null : createLiveKitTransport();
5863
+ const controller = createRinggWidgetController(config, {
5864
+ urlResolver: defaultUrlResolver,
5865
+ micPermission: createNativeMicPermission(),
5866
+ ...ports,
5867
+ transport: ports?.transport ?? livekit.transport
5868
+ });
5869
+ return { controller, livekit };
5870
+ }, [config]);
5871
+ useEffect16(
5872
+ () => () => {
5873
+ stack.controller.destroy();
5874
+ stack.livekit?.dispose();
5875
+ },
5876
+ [stack]
5877
+ );
5878
+ const onReadyRef = useRef15(onReady);
5879
+ onReadyRef.current = onReady;
5880
+ useEffect16(() => {
5881
+ onReadyRef.current?.(stack.controller);
5882
+ }, [stack]);
5883
+ return /* @__PURE__ */ jsx28(ControlledRinggWidget, { controller: stack.controller, room: stack.livekit?.room });
5594
5884
  };
5595
- var createNativeMicPermission = () => ({
5596
- async isGranted() {
5597
- if (Platform6.OS === "android") {
5598
- return PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO);
5599
- }
5600
- return probeMicrophone();
5885
+ var RinggWidget = (props) => props.config ? /* @__PURE__ */ jsx28(ManagedRinggWidget, { config: props.config, ports: props.ports, onReady: props.onReady }) : /* @__PURE__ */ jsx28(ControlledRinggWidget, { controller: props.controller, room: props.room });
5886
+ var styles24 = StyleSheet24.create({
5887
+ overlay: { ...StyleSheet24.absoluteFill, justifyContent: "flex-end", alignItems: "flex-end" },
5888
+ panelAnchor: { marginHorizontal: EDGE_INSET, marginTop: EDGE_INSET, marginBottom: BOTTOM_SAFE_INSET },
5889
+ panel: {
5890
+ borderWidth: StyleSheet24.hairlineWidth,
5891
+ // `overflow: hidden` is what keeps the transcript, the header bar and the
5892
+ // rounded corners agreeing with each other.
5893
+ overflow: "hidden",
5894
+ // Web stacks three shadows at 1-2% alpha — the panel sits just off the page
5895
+ // rather than floating above it. 12% read as a hard slab, and on Android
5896
+ // `elevation` is the only one of these that applies, so it was the entire
5897
+ // effect instead of a supporting layer.
5898
+ shadowColor: "#000000",
5899
+ shadowOpacity: 0.05,
5900
+ shadowRadius: 20,
5901
+ shadowOffset: { width: 0, height: 8 },
5902
+ elevation: 4
5601
5903
  },
5602
- async request() {
5603
- if (Platform6.OS === "android") {
5604
- const result = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, ANDROID_RATIONALE);
5605
- return result === PermissionsAndroid.RESULTS.GRANTED;
5606
- }
5607
- return probeMicrophone();
5608
- }
5904
+ trigger: { position: "absolute", right: EDGE_INSET, bottom: BOTTOM_SAFE_INSET },
5905
+ triggerSurface: { width: TRIGGER_SIZE, height: TRIGGER_SIZE, borderRadius: TRIGGER_RADIUS, alignItems: "center", justifyContent: "center" },
5906
+ chromeButton: { position: "absolute", top: 10, zIndex: 20, width: 28, height: 28, borderRadius: 14, alignItems: "center", justifyContent: "center" },
5907
+ minimizeButton: { right: 42 },
5908
+ closeButton: { right: 10 },
5909
+ body: { flex: 1 },
5910
+ bodyLive: { paddingHorizontal: 20, paddingVertical: 8, gap: 10 },
5911
+ bodyIdle: { padding: 24, gap: 12 },
5912
+ bodySpacer: { flex: 1, marginTop: 56 },
5913
+ voiceWrapper: { width: "100%", alignItems: "stretch", justifyContent: "center", marginTop: 56 },
5914
+ voiceWrapperCompact: { height: 56, flexShrink: 0 },
5915
+ voiceWrapperFull: { flex: 1, minHeight: 0 },
5916
+ transcript: { flex: 1 },
5917
+ // `justifyContent: flex-end` is web's `mt-auto`: a short conversation sits at
5918
+ // the bottom of the panel rather than floating at the top.
5919
+ transcriptList: { flexGrow: 1, justifyContent: "flex-end", gap: 12 },
5920
+ transcriptTopClearance: { paddingTop: 64 },
5921
+ transcriptBottomClearance: { paddingBottom: 80 },
5922
+ fullWidth: { width: "100%" },
5923
+ controlsBar: { position: "absolute", zIndex: 10, bottom: 0, left: 0, right: 0, paddingHorizontal: 20, paddingVertical: 12 },
5924
+ error: { textAlign: "center", fontSize: 14, marginBottom: 8 },
5925
+ connecting: { fontSize: 14, textAlign: "center" },
5926
+ startActions: { flexDirection: "row", alignItems: "center", gap: 8, width: "100%" },
5927
+ legal: { fontSize: 12, lineHeight: 20, textAlign: "center" }
5609
5928
  });
5610
5929
 
5611
5930
  // src/platform/app-origin.ts
@@ -5673,26 +5992,73 @@ var createHostActionDispatcher = (onAction) => (action, onLog) => {
5673
5992
  }
5674
5993
  };
5675
5994
  export {
5995
+ CHAT_WIDGET_AFTER_TEXT_MS,
5996
+ CHAT_WIDGET_GRACE_MS,
5997
+ WIDGET_DEFAULTS as DEFAULT_CONFIG,
5998
+ DEFAULT_ENVIRONMENT_URLS,
5999
+ DEFAULT_WIDGET_THEME,
5676
6000
  GradientFill,
6001
+ MIN_TYPING_INDICATOR_MS,
5677
6002
  Markdown,
5678
6003
  RinggWidget,
6004
+ WIDGET_PRESET_THEMES,
5679
6005
  WidgetThemeProvider,
5680
6006
  appOrigin,
6007
+ buildPayload,
6008
+ colorToBackground,
6009
+ colorToBorder,
6010
+ colorToSolid,
5681
6011
  createAudioSession,
5682
6012
  createCallbackEventBus,
6013
+ createComponentStore,
6014
+ createDomEventBus,
5683
6015
  createHostActionDispatcher,
5684
6016
  createLiveKitTransport,
6017
+ createMessageStore,
5685
6018
  createNativeMicPermission,
5686
6019
  createNotificationPlayer,
6020
+ createRinggApiClient,
5687
6021
  createRinggWidgetController,
6022
+ createSessionStore,
6023
+ createShellStore,
5688
6024
  createSilentNotificationPlayer,
6025
+ createSlashCommandStore,
6026
+ createStaticUrlResolver,
6027
+ createStore,
6028
+ createSystemClock,
6029
+ createTypingStore,
6030
+ defaultUrlResolver,
6031
+ extractColorsFromGradient,
6032
+ formatBlocksAction,
6033
+ formatComponentResponse,
6034
+ formatDynamicData,
6035
+ formatSlashCommand,
6036
+ formatSlotDate,
6037
+ formatSlotTime,
6038
+ getButtonRadius,
6039
+ getContrastingTextColor,
6040
+ getDominantColor,
6041
+ grantedMicPermission,
6042
+ groupSlotsByDate,
6043
+ isBlocks,
6044
+ isButtons,
6045
+ isCalendarBooking,
6046
+ isConfirmation,
6047
+ isForm,
6048
+ isGradient,
6049
+ isInteractiveFlow,
6050
+ isLightColor,
6051
+ mergeComponentTheme,
6052
+ mergeWidgetTheme,
5689
6053
  resolveButtonRadius,
5690
6054
  resolveFontFamily,
5691
6055
  resolveRadius,
5692
6056
  ringgId,
6057
+ silentNotificationPlayer,
5693
6058
  solidColor,
5694
6059
  toSize,
5695
6060
  toViewStyle,
6061
+ transformRpcToComponent,
5696
6062
  useRinggComponents,
5697
6063
  useRinggMessages,
5698
6064
  useRinggSession,