@blario/react-native 0.0.0 → 0.1.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.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +378 -0
  3. package/dist/BlarProvider.d.ts +74 -0
  4. package/dist/actions/batched.d.ts +1 -0
  5. package/dist/actions/click.d.ts +4 -0
  6. package/dist/actions/commit-watch.d.ts +15 -0
  7. package/dist/actions/events.d.ts +35 -0
  8. package/dist/actions/executor.d.ts +29 -0
  9. package/dist/actions/highlight.d.ts +4 -0
  10. package/dist/actions/interaction-guards.d.ts +9 -0
  11. package/dist/actions/landing.d.ts +11 -0
  12. package/dist/actions/navigation-driver.d.ts +22 -0
  13. package/dist/actions/parse.d.ts +4 -0
  14. package/dist/actions/result.d.ts +4 -0
  15. package/dist/actions/scrolling.d.ts +12 -0
  16. package/dist/actions/services.d.ts +20 -0
  17. package/dist/actions/settle.d.ts +13 -0
  18. package/dist/actions/switch.d.ts +3 -0
  19. package/dist/actions/target.d.ts +25 -0
  20. package/dist/actions/text-input.d.ts +9 -0
  21. package/dist/actions/types.d.ts +59 -0
  22. package/dist/agent/batch-commands.d.ts +17 -0
  23. package/dist/agent/compose.d.ts +23 -0
  24. package/dist/agent/controller.d.ts +28 -0
  25. package/dist/agent/frame-runner.d.ts +9 -0
  26. package/dist/agent/frames.d.ts +180 -0
  27. package/dist/agent/handlers/agent-command.d.ts +5 -0
  28. package/dist/agent/handlers/batch.d.ts +5 -0
  29. package/dist/agent/handlers/consent-screenshot.d.ts +4 -0
  30. package/dist/agent/handlers/context.d.ts +16 -0
  31. package/dist/agent/handlers/dom-state.d.ts +4 -0
  32. package/dist/agent/handlers/element-context.d.ts +2 -0
  33. package/dist/agent/handlers/screenshot.d.ts +3 -0
  34. package/dist/agent/handlers/text-dom.d.ts +5 -0
  35. package/dist/agent/stage.d.ts +13 -0
  36. package/dist/agent-mode/action-description.d.ts +3 -0
  37. package/dist/agent-mode/bubbles.d.ts +10 -0
  38. package/dist/agent-mode/colors.d.ts +12 -0
  39. package/dist/agent-mode/controller.d.ts +92 -0
  40. package/dist/agent-mode/cursor-follow.d.ts +12 -0
  41. package/dist/agent-mode/cursor.d.ts +37 -0
  42. package/dist/agent-mode/frames.d.ts +36 -0
  43. package/dist/agent-mode/haptics.d.ts +6 -0
  44. package/dist/agent-mode/keep-awake.d.ts +19 -0
  45. package/dist/agent-mode/layer-stacking.d.ts +7 -0
  46. package/dist/agent-mode/markdown.d.ts +19 -0
  47. package/dist/agent-mode/panel-geometry.d.ts +6 -0
  48. package/dist/agent-mode/resume.d.ts +13 -0
  49. package/dist/agent-mode/run-memory.d.ts +19 -0
  50. package/dist/agent-mode/spotlight-controller.d.ts +68 -0
  51. package/dist/agent-mode/spotlight.d.ts +6 -0
  52. package/dist/agent-mode/state.d.ts +9 -0
  53. package/dist/agent-mode/status.d.ts +11 -0
  54. package/dist/agent-mode/timers.d.ts +6 -0
  55. package/dist/agent-mode/ui/AgentDock.d.ts +37 -0
  56. package/dist/agent-mode/ui/AgentLayer.d.ts +19 -0
  57. package/dist/agent-mode/ui/AgentLayerHost.d.ts +8 -0
  58. package/dist/agent-mode/ui/AgentPanel.d.ts +19 -0
  59. package/dist/agent-mode/ui/AgentTap.d.ts +10 -0
  60. package/dist/agent-mode/ui/MarkdownText.d.ts +8 -0
  61. package/dist/agent-mode/ui/MessageBubbles.d.ts +9 -0
  62. package/dist/agent-mode/ui/PendingOverlay.d.ts +5 -0
  63. package/dist/agent-mode/ui/SpotlightLayer.d.ts +9 -0
  64. package/dist/agent-mode/ui/SpotlightOverlay.d.ts +7 -0
  65. package/dist/agent-mode/ui/StatusCard.d.ts +14 -0
  66. package/dist/agent-mode/ui/TargetPulse.d.ts +9 -0
  67. package/dist/agent-mode/ui/TouchShield.d.ts +5 -0
  68. package/dist/agent-mode/ui/WarningIcon.d.ts +6 -0
  69. package/dist/agent-mode/ui/full-window-overlay.d.ts +5 -0
  70. package/dist/agent-mode/ui/layer-host.d.ts +2 -0
  71. package/dist/backend/api-client.d.ts +38 -0
  72. package/dist/backend/api-types.d.ts +25 -0
  73. package/dist/backend/app-info.d.ts +14 -0
  74. package/dist/backend/backend-transport.d.ts +70 -0
  75. package/dist/backend/backoff.d.ts +9 -0
  76. package/dist/backend/base64.d.ts +1 -0
  77. package/dist/backend/errors.d.ts +17 -0
  78. package/dist/backend/foreground.d.ts +1 -0
  79. package/dist/backend/health.d.ts +17 -0
  80. package/dist/backend/identity-proof.d.ts +23 -0
  81. package/dist/backend/identity.d.ts +27 -0
  82. package/dist/backend/message-metadata.d.ts +19 -0
  83. package/dist/backend/presence.d.ts +20 -0
  84. package/dist/backend/response-cache.d.ts +9 -0
  85. package/dist/backend/session-change.d.ts +2 -0
  86. package/dist/backend/session-socket.d.ts +49 -0
  87. package/dist/backend/session.d.ts +39 -0
  88. package/dist/backend/socket-url.d.ts +9 -0
  89. package/dist/backend/storage.d.ts +13 -0
  90. package/dist/backend/tab-id.d.ts +2 -0
  91. package/dist/blar.d.ts +37 -0
  92. package/dist/chat/ChatLayer.d.ts +29 -0
  93. package/dist/chat/ChatSheet.d.ts +11 -0
  94. package/dist/chat/ChatView.d.ts +19 -0
  95. package/dist/chat/Launcher.d.ts +14 -0
  96. package/dist/chat/bridge.d.ts +22 -0
  97. package/dist/chat/chat-coordinator.d.ts +57 -0
  98. package/dist/chat/chat-state.d.ts +20 -0
  99. package/dist/chat/navigation.d.ts +5 -0
  100. package/dist/chat/options.d.ts +22 -0
  101. package/dist/chat/webview.d.ts +10 -0
  102. package/dist/chat/widget-bundle.generated.d.ts +2 -0
  103. package/dist/chat/widget-shell.d.ts +24 -0
  104. package/dist/confirmation/port.d.ts +12 -0
  105. package/dist/fiber/locate.d.ts +7 -0
  106. package/dist/fiber/narrow.d.ts +5 -0
  107. package/dist/fiber/reader.d.ts +10 -0
  108. package/dist/fiber/text.d.ts +4 -0
  109. package/dist/fiber/types.d.ts +21 -0
  110. package/dist/host/border-radius.d.ts +1 -0
  111. package/dist/host/instance.d.ts +9 -0
  112. package/dist/host/measure.d.ts +3 -0
  113. package/dist/host/rect.d.ts +22 -0
  114. package/dist/host/visible-area.d.ts +2 -0
  115. package/dist/index.d.ts +10 -0
  116. package/dist/nudge/NudgeBubble.d.ts +15 -0
  117. package/dist/nudge/nudge.d.ts +23 -0
  118. package/dist/optional/expo-haptics.d.ts +1 -0
  119. package/dist/optional/expo-keep-awake.d.ts +1 -0
  120. package/dist/optional/react-native-haptic-feedback.d.ts +1 -0
  121. package/dist/optional/react-native-keep-awake.d.ts +1 -0
  122. package/dist/overlay/AgentOverlay.d.ts +5 -0
  123. package/dist/screenshot/capture-backend.d.ts +6 -0
  124. package/dist/screenshot/screenshotter.d.ts +18 -0
  125. package/dist/screenshot/skia.d.ts +35 -0
  126. package/dist/screenshot/view-shot.d.ts +2 -0
  127. package/dist/snapshot/capture.d.ts +36 -0
  128. package/dist/snapshot/classify.d.ts +11 -0
  129. package/dist/snapshot/collect.d.ts +27 -0
  130. package/dist/snapshot/element-context.d.ts +9 -0
  131. package/dist/snapshot/icon.d.ts +2 -0
  132. package/dist/snapshot/kinds.d.ts +3 -0
  133. package/dist/snapshot/modals.d.ts +2 -0
  134. package/dist/snapshot/navigation.d.ts +14 -0
  135. package/dist/snapshot/ordering.d.ts +2 -0
  136. package/dist/snapshot/properties.d.ts +14 -0
  137. package/dist/snapshot/registry.d.ts +16 -0
  138. package/dist/snapshot/render.d.ts +10 -0
  139. package/dist/snapshot/route-summary.d.ts +3 -0
  140. package/dist/snapshot/scroll.d.ts +15 -0
  141. package/dist/snapshot/text-layout.d.ts +8 -0
  142. package/dist/snapshot/view.d.ts +30 -0
  143. package/dist/snapshot/visibility.d.ts +17 -0
  144. package/dist/snapshot/wrappers.d.ts +11 -0
  145. package/dist/telemetry/SdkErrorBoundary.d.ts +15 -0
  146. package/dist/telemetry/build-info.generated.d.ts +2 -0
  147. package/dist/telemetry/envelope.d.ts +53 -0
  148. package/dist/telemetry/reporter.d.ts +33 -0
  149. package/dist/telemetry/scrub.d.ts +1 -0
  150. package/dist/telemetry/sdk-reporter.d.ts +5 -0
  151. package/dist/transport/connection-options.d.ts +18 -0
  152. package/dist/transport/relay-transport.d.ts +17 -0
  153. package/dist/transport/transport.d.ts +7 -0
  154. package/dist/ui/insets.d.ts +8 -0
  155. package/dist/ui/strings.d.ts +33 -0
  156. package/dist/ui/useInsets.d.ts +2 -0
  157. package/dist/version.d.ts +1 -0
  158. package/package.json +84 -3
  159. package/src/BlarProvider.tsx +462 -0
  160. package/src/actions/batched.ts +16 -0
  161. package/src/actions/click.ts +54 -0
  162. package/src/actions/commit-watch.ts +86 -0
  163. package/src/actions/events.ts +58 -0
  164. package/src/actions/executor.ts +147 -0
  165. package/src/actions/highlight.ts +13 -0
  166. package/src/actions/interaction-guards.ts +44 -0
  167. package/src/actions/landing.ts +26 -0
  168. package/src/actions/navigation-driver.ts +131 -0
  169. package/src/actions/parse.ts +82 -0
  170. package/src/actions/result.ts +14 -0
  171. package/src/actions/scrolling.ts +112 -0
  172. package/src/actions/services.ts +24 -0
  173. package/src/actions/settle.ts +40 -0
  174. package/src/actions/switch.ts +31 -0
  175. package/src/actions/target.ts +77 -0
  176. package/src/actions/text-input.ts +82 -0
  177. package/src/actions/types.ts +52 -0
  178. package/src/agent/batch-commands.ts +33 -0
  179. package/src/agent/compose.ts +67 -0
  180. package/src/agent/controller.ts +106 -0
  181. package/src/agent/frame-runner.ts +27 -0
  182. package/src/agent/frames.ts +193 -0
  183. package/src/agent/handlers/agent-command.ts +74 -0
  184. package/src/agent/handlers/batch.ts +72 -0
  185. package/src/agent/handlers/consent-screenshot.ts +44 -0
  186. package/src/agent/handlers/context.ts +17 -0
  187. package/src/agent/handlers/dom-state.ts +19 -0
  188. package/src/agent/handlers/element-context.ts +9 -0
  189. package/src/agent/handlers/screenshot.ts +24 -0
  190. package/src/agent/handlers/text-dom.ts +17 -0
  191. package/src/agent/stage.ts +22 -0
  192. package/src/agent-mode/action-description.ts +25 -0
  193. package/src/agent-mode/bubbles.ts +31 -0
  194. package/src/agent-mode/colors.ts +40 -0
  195. package/src/agent-mode/controller.ts +459 -0
  196. package/src/agent-mode/cursor-follow.ts +43 -0
  197. package/src/agent-mode/cursor.ts +118 -0
  198. package/src/agent-mode/frames.ts +70 -0
  199. package/src/agent-mode/haptics.ts +49 -0
  200. package/src/agent-mode/keep-awake.ts +107 -0
  201. package/src/agent-mode/layer-stacking.ts +16 -0
  202. package/src/agent-mode/markdown.ts +84 -0
  203. package/src/agent-mode/panel-geometry.ts +27 -0
  204. package/src/agent-mode/resume.ts +16 -0
  205. package/src/agent-mode/run-memory.ts +65 -0
  206. package/src/agent-mode/spotlight-controller.ts +255 -0
  207. package/src/agent-mode/spotlight.ts +14 -0
  208. package/src/agent-mode/state.ts +36 -0
  209. package/src/agent-mode/status.ts +13 -0
  210. package/src/agent-mode/timers.ts +11 -0
  211. package/src/agent-mode/ui/AgentDock.tsx +285 -0
  212. package/src/agent-mode/ui/AgentLayer.tsx +123 -0
  213. package/src/agent-mode/ui/AgentLayerHost.tsx +37 -0
  214. package/src/agent-mode/ui/AgentPanel.tsx +187 -0
  215. package/src/agent-mode/ui/AgentTap.tsx +190 -0
  216. package/src/agent-mode/ui/MarkdownText.tsx +55 -0
  217. package/src/agent-mode/ui/MessageBubbles.tsx +58 -0
  218. package/src/agent-mode/ui/PendingOverlay.tsx +32 -0
  219. package/src/agent-mode/ui/SpotlightLayer.tsx +60 -0
  220. package/src/agent-mode/ui/SpotlightOverlay.tsx +35 -0
  221. package/src/agent-mode/ui/StatusCard.tsx +137 -0
  222. package/src/agent-mode/ui/TargetPulse.tsx +74 -0
  223. package/src/agent-mode/ui/TouchShield.tsx +22 -0
  224. package/src/agent-mode/ui/WarningIcon.tsx +32 -0
  225. package/src/agent-mode/ui/full-window-overlay.ts +16 -0
  226. package/src/agent-mode/ui/layer-host.ts +11 -0
  227. package/src/backend/api-client.ts +210 -0
  228. package/src/backend/api-types.ts +56 -0
  229. package/src/backend/app-info.ts +40 -0
  230. package/src/backend/backend-transport.ts +338 -0
  231. package/src/backend/backoff.ts +16 -0
  232. package/src/backend/base64.ts +25 -0
  233. package/src/backend/errors.ts +32 -0
  234. package/src/backend/foreground.ts +10 -0
  235. package/src/backend/health.ts +64 -0
  236. package/src/backend/identity-proof.ts +151 -0
  237. package/src/backend/identity.ts +31 -0
  238. package/src/backend/message-metadata.ts +28 -0
  239. package/src/backend/presence.ts +95 -0
  240. package/src/backend/response-cache.ts +45 -0
  241. package/src/backend/session-change.ts +14 -0
  242. package/src/backend/session-socket.ts +292 -0
  243. package/src/backend/session.ts +188 -0
  244. package/src/backend/socket-url.ts +25 -0
  245. package/src/backend/storage.ts +49 -0
  246. package/src/backend/tab-id.ts +24 -0
  247. package/src/blar.ts +100 -0
  248. package/src/chat/ChatLayer.tsx +109 -0
  249. package/src/chat/ChatSheet.tsx +82 -0
  250. package/src/chat/ChatView.tsx +127 -0
  251. package/src/chat/Launcher.tsx +72 -0
  252. package/src/chat/bridge.ts +56 -0
  253. package/src/chat/chat-coordinator.ts +189 -0
  254. package/src/chat/chat-state.ts +35 -0
  255. package/src/chat/navigation.ts +15 -0
  256. package/src/chat/options.ts +39 -0
  257. package/src/chat/webview.ts +23 -0
  258. package/src/chat/widget-bundle.generated.ts +2 -0
  259. package/src/chat/widget-shell.ts +170 -0
  260. package/src/confirmation/port.ts +15 -0
  261. package/src/fiber/locate.ts +41 -0
  262. package/src/fiber/narrow.ts +15 -0
  263. package/src/fiber/reader.ts +98 -0
  264. package/src/fiber/text.ts +52 -0
  265. package/src/fiber/types.ts +24 -0
  266. package/src/host/border-radius.ts +11 -0
  267. package/src/host/instance.ts +30 -0
  268. package/src/host/measure.ts +25 -0
  269. package/src/host/rect.ts +45 -0
  270. package/src/host/visible-area.ts +5 -0
  271. package/src/index.ts +10 -0
  272. package/src/nudge/NudgeBubble.tsx +117 -0
  273. package/src/nudge/nudge.ts +100 -0
  274. package/src/optional/expo-haptics.ts +11 -0
  275. package/src/optional/expo-keep-awake.ts +11 -0
  276. package/src/optional/react-native-haptic-feedback.ts +11 -0
  277. package/src/optional/react-native-keep-awake.ts +11 -0
  278. package/src/overlay/AgentOverlay.tsx +16 -0
  279. package/src/screenshot/capture-backend.ts +7 -0
  280. package/src/screenshot/screenshotter.ts +39 -0
  281. package/src/screenshot/skia.ts +107 -0
  282. package/src/screenshot/view-shot.ts +41 -0
  283. package/src/snapshot/capture.ts +163 -0
  284. package/src/snapshot/classify.ts +40 -0
  285. package/src/snapshot/collect.ts +136 -0
  286. package/src/snapshot/element-context.ts +66 -0
  287. package/src/snapshot/icon.ts +21 -0
  288. package/src/snapshot/kinds.ts +10 -0
  289. package/src/snapshot/modals.ts +21 -0
  290. package/src/snapshot/navigation.ts +47 -0
  291. package/src/snapshot/ordering.ts +26 -0
  292. package/src/snapshot/properties.ts +71 -0
  293. package/src/snapshot/registry.ts +33 -0
  294. package/src/snapshot/render.ts +140 -0
  295. package/src/snapshot/route-summary.ts +15 -0
  296. package/src/snapshot/scroll.ts +90 -0
  297. package/src/snapshot/text-layout.ts +59 -0
  298. package/src/snapshot/view.ts +87 -0
  299. package/src/snapshot/visibility.ts +63 -0
  300. package/src/snapshot/wrappers.ts +69 -0
  301. package/src/telemetry/SdkErrorBoundary.tsx +28 -0
  302. package/src/telemetry/build-info.generated.ts +2 -0
  303. package/src/telemetry/envelope.ts +83 -0
  304. package/src/telemetry/reporter.ts +170 -0
  305. package/src/telemetry/scrub.ts +10 -0
  306. package/src/telemetry/sdk-reporter.ts +37 -0
  307. package/src/transport/connection-options.ts +65 -0
  308. package/src/transport/relay-transport.ts +82 -0
  309. package/src/transport/transport.ts +8 -0
  310. package/src/ui/insets.ts +31 -0
  311. package/src/ui/strings.ts +102 -0
  312. package/src/ui/useInsets.ts +10 -0
  313. package/src/version.ts +1 -0
@@ -0,0 +1,24 @@
1
+ const ID_LENGTH = 16;
2
+ const ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789';
3
+
4
+ function randomSegment(length: number): string {
5
+ let segment = '';
6
+ for (let index = 0; index < length; index += 1) {
7
+ segment += ALPHABET.charAt(Math.floor(Math.random() * ALPHABET.length));
8
+ }
9
+ return segment;
10
+ }
11
+
12
+ let processTabId: string | null = null;
13
+
14
+ export function getTabId(): string {
15
+ processTabId = processTabId ?? `rn-${randomSegment(ID_LENGTH)}`;
16
+ return processTabId;
17
+ }
18
+
19
+ export function generateIdempotencyKey(): string {
20
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (placeholder) => {
21
+ const digit = Math.floor(Math.random() * 16);
22
+ return (placeholder === 'x' ? digit : (digit & 0x3) | 0x8).toString(16);
23
+ });
24
+ }
package/src/blar.ts ADDED
@@ -0,0 +1,100 @@
1
+ import type { BackendTransport } from './backend/backend-transport.ts';
2
+ import type { BlarIdentity } from './backend/identity.ts';
3
+ import type { MessageMetadata } from './backend/message-metadata.ts';
4
+
5
+ export interface MessageMetadataOptions {
6
+ includeTextDom: boolean;
7
+ }
8
+
9
+ export interface ChatControls {
10
+ open: () => void;
11
+ close: () => void;
12
+ getUnread: () => number;
13
+ isOpen: () => boolean;
14
+ }
15
+
16
+ export type MessageMetadataReader = (options: MessageMetadataOptions) => Promise<MessageMetadata | null>;
17
+
18
+ class BlarClient {
19
+ private transport: BackendTransport | null = null;
20
+
21
+ private metadataReader: MessageMetadataReader | null = null;
22
+
23
+ private chat: ChatControls | null = null;
24
+
25
+ private pendingUser: BlarIdentity | null = null;
26
+
27
+ private logoutRequested = false;
28
+
29
+ attach(transport: BackendTransport, metadataReader: MessageMetadataReader): void {
30
+ this.transport = transport;
31
+ this.metadataReader = metadataReader;
32
+ }
33
+
34
+ detach(transport: BackendTransport): void {
35
+ if (this.transport === transport) {
36
+ this.transport = null;
37
+ this.metadataReader = null;
38
+ }
39
+ }
40
+
41
+ attachChat(controls: ChatControls): void {
42
+ this.chat = controls;
43
+ }
44
+
45
+ detachChat(controls: ChatControls): void {
46
+ if (this.chat === controls) {
47
+ this.chat = null;
48
+ }
49
+ }
50
+
51
+ open(): void {
52
+ this.chat?.open();
53
+ }
54
+
55
+ close(): void {
56
+ this.chat?.close();
57
+ }
58
+
59
+ getUnreadCount(): number {
60
+ return this.chat?.getUnread() ?? 0;
61
+ }
62
+
63
+ isChatOpen(): boolean {
64
+ return this.chat?.isOpen() ?? false;
65
+ }
66
+
67
+ async setUser(identity: BlarIdentity): Promise<void> {
68
+ this.logoutRequested = false;
69
+ this.pendingUser = identity;
70
+ await this.transport?.setUser(identity);
71
+ }
72
+
73
+ async logout(): Promise<void> {
74
+ this.pendingUser = null;
75
+ this.logoutRequested = true;
76
+ await this.transport?.logout();
77
+ }
78
+
79
+ consumePendingUser(): BlarIdentity | null {
80
+ return this.logoutRequested ? null : this.pendingUser;
81
+ }
82
+
83
+ isConnected(): boolean {
84
+ return this.transport?.isConnected() ?? false;
85
+ }
86
+
87
+ getTabId(): string | null {
88
+ return this.transport?.getTabId() ?? null;
89
+ }
90
+
91
+ getSessionState(): ReturnType<BackendTransport['getState']> | null {
92
+ return this.transport?.getState() ?? null;
93
+ }
94
+
95
+ getMessageMetadata(options: MessageMetadataOptions = { includeTextDom: false }): Promise<MessageMetadata | null> {
96
+ return this.metadataReader === null ? Promise.resolve(null) : this.metadataReader(options);
97
+ }
98
+ }
99
+
100
+ export const Blar = new BlarClient();
@@ -0,0 +1,109 @@
1
+ import React, { useMemo } from 'react';
2
+
3
+ import type { WidgetProof } from '../backend/backend-transport.ts';
4
+ import type { BlarIdentity } from '../backend/identity.ts';
5
+ import type { MessageMetadata } from '../backend/message-metadata.ts';
6
+ import { NudgeBubble } from '../nudge/NudgeBubble.tsx';
7
+ import type { Language, UiStrings } from '../ui/strings.ts';
8
+ import { ChatSheet } from './ChatSheet.tsx';
9
+ import { ChatView } from './ChatView.tsx';
10
+ import { Launcher } from './Launcher.tsx';
11
+ import type { ChatSnapshot } from './chat-coordinator.ts';
12
+ import { shouldShowLauncher } from './chat-state.ts';
13
+ import type { ResolvedChatOptions } from './options.ts';
14
+ import type { ShellIdentity } from './widget-shell.ts';
15
+ import { WIDGET_BUNDLE } from './widget-bundle.generated.ts';
16
+
17
+ export interface ChatLayerProps {
18
+ snapshot: ChatSnapshot;
19
+ options: ResolvedChatOptions;
20
+ strings: UiStrings;
21
+ language: Language;
22
+ publishableKey: string;
23
+ apiBaseUrl: string;
24
+ tabId: string;
25
+ identity: BlarIdentity | null;
26
+ getMetadata: (options: { includeTextDom: boolean }) => Promise<MessageMetadata | null>;
27
+ getProof: () => Promise<WidgetProof | null>;
28
+ onOpen: () => void;
29
+ onClose: () => void;
30
+ onSessionChanged: (sessionId: number | null) => void;
31
+ onAcceptNudge: () => void;
32
+ onDeclineNudge: () => void;
33
+ onSnoozeNudge: () => void;
34
+ log?: (message: string) => void;
35
+ }
36
+
37
+ function toShellIdentity(identity: BlarIdentity | null): ShellIdentity | null {
38
+ if (identity === null) {
39
+ return null;
40
+ }
41
+ return {
42
+ externalId: identity.externalId,
43
+ email: identity.email,
44
+ name: identity.name,
45
+ phone: identity.phone,
46
+ contactCompany: identity.contactCompany,
47
+ contactCompanyExternalId: identity.contactCompanyExternalId,
48
+ country: identity.country,
49
+ customFields: identity.customFields,
50
+ identityToken: identity.identityToken,
51
+ identitySignature: identity.identitySignature,
52
+ providesProof: identity.getProof !== undefined || identity.getIdentityToken !== undefined || identity.getIdentitySignature !== undefined,
53
+ };
54
+ }
55
+
56
+ export function ChatLayer(props: ChatLayerProps): React.JSX.Element {
57
+ const { snapshot, options, strings, identity } = props;
58
+ const shellIdentity = useMemo(() => toShellIdentity(identity), [identity]);
59
+ const shell = useMemo(
60
+ () => ({
61
+ bundle: options.scriptUrl === null ? WIDGET_BUNDLE : null,
62
+ scriptUrl: options.scriptUrl,
63
+ publishableKey: props.publishableKey,
64
+ apiBaseUrl: props.apiBaseUrl,
65
+ language: props.language,
66
+ tabId: props.tabId,
67
+ primaryColor: options.widgetColor,
68
+ identity: shellIdentity,
69
+ }),
70
+ [options.scriptUrl, options.widgetColor, props.publishableKey, props.apiBaseUrl, props.language, props.tabId, shellIdentity],
71
+ );
72
+ const { chat, nudge, chatKey, mounted, sessionRequest } = snapshot;
73
+ const launcherShown = options.launcherVisible && shouldShowLauncher(chat);
74
+ return (
75
+ <>
76
+ <ChatSheet mode={chat.mode} mounted={mounted} strings={strings} onClose={props.onClose}>
77
+ {mounted ? (
78
+ <ChatView
79
+ key={chatKey}
80
+ shell={shell}
81
+ unavailableText={strings.chatUnavailable}
82
+ getMetadata={props.getMetadata}
83
+ getProof={props.getProof}
84
+ onClosed={props.onClose}
85
+ onSessionChanged={props.onSessionChanged}
86
+ open={chat.mode === 'open'}
87
+ sessionRequest={sessionRequest}
88
+ log={props.log}
89
+ />
90
+ ) : null}
91
+ </ChatSheet>
92
+ {launcherShown ? (
93
+ <Launcher unread={chat.unread} label={strings.chatLauncher} color={options.primaryColor} placement={options.placement} onPress={props.onOpen} />
94
+ ) : null}
95
+ {nudge === null || chat.mode === 'open' || chat.agentWorking ? null : (
96
+ <NudgeBubble
97
+ nudge={nudge}
98
+ strings={strings}
99
+ color={options.primaryColor}
100
+ placement={options.placement}
101
+ launcherVisible={launcherShown}
102
+ onAccept={props.onAcceptNudge}
103
+ onDecline={props.onDeclineNudge}
104
+ onLater={props.onSnoozeNudge}
105
+ />
106
+ )}
107
+ </>
108
+ );
109
+ }
@@ -0,0 +1,82 @@
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Animated, BackHandler, Dimensions, Easing, KeyboardAvoidingView, Platform, Pressable, StyleSheet, View } from 'react-native';
3
+
4
+ import { useInsets } from '../ui/useInsets.ts';
5
+ import type { UiStrings } from '../ui/strings.ts';
6
+ import type { SheetMode } from './chat-state.ts';
7
+
8
+ export interface ChatSheetProps {
9
+ mode: SheetMode;
10
+ mounted: boolean;
11
+ strings: UiStrings;
12
+ onClose: () => void;
13
+ children: React.ReactNode;
14
+ }
15
+
16
+ const TOP_GAP = 12;
17
+ const ANIMATION_MS = 240;
18
+
19
+ export function ChatSheet({ mode, mounted, strings, onClose, children }: ChatSheetProps): React.JSX.Element | null {
20
+ const insets = useInsets();
21
+ const windowHeight = Dimensions.get('window').height;
22
+ const sheetHeight = windowHeight - insets.top - TOP_GAP;
23
+ const progress = useRef(new Animated.Value(mode === 'open' ? 1 : 0)).current;
24
+ const isOpen = mode === 'open';
25
+
26
+ useEffect(() => {
27
+ Animated.timing(progress, { toValue: isOpen ? 1 : 0, duration: ANIMATION_MS, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
28
+ }, [isOpen, progress]);
29
+
30
+ useEffect(() => {
31
+ if (!isOpen) {
32
+ return undefined;
33
+ }
34
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
35
+ onClose();
36
+ return true;
37
+ });
38
+ return () => subscription.remove();
39
+ }, [isOpen, onClose]);
40
+
41
+ if (!mounted) {
42
+ return null;
43
+ }
44
+ const translateY = progress.interpolate({ inputRange: [0, 1], outputRange: [sheetHeight + 48, 0] });
45
+ const backdropOpacity = progress.interpolate({ inputRange: [0, 1], outputRange: [0, 0.35] });
46
+ return (
47
+ <View style={StyleSheet.absoluteFill} pointerEvents={isOpen ? 'auto' : 'none'}>
48
+ <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]}>
49
+ <Pressable accessibilityLabel={strings.chatClose} style={StyleSheet.absoluteFill} onPress={onClose} />
50
+ </Animated.View>
51
+ <KeyboardAvoidingView
52
+ style={[styles.container, { paddingTop: insets.top + TOP_GAP }]}
53
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
54
+ pointerEvents="box-none"
55
+ >
56
+ <Animated.View
57
+ accessibilityElementsHidden={!isOpen}
58
+ importantForAccessibility={isOpen ? 'yes' : 'no-hide-descendants'}
59
+ style={[styles.sheet, { height: sheetHeight, paddingBottom: insets.bottom, transform: [{ translateY }] }]}
60
+ >
61
+ {children}
62
+ </Animated.View>
63
+ </KeyboardAvoidingView>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ const styles = StyleSheet.create({
69
+ backdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000000' },
70
+ container: { flex: 1, justifyContent: 'flex-end' },
71
+ sheet: {
72
+ flexShrink: 1,
73
+ backgroundColor: '#ffffff',
74
+ borderTopLeftRadius: 18,
75
+ borderTopRightRadius: 18,
76
+ overflow: 'hidden',
77
+ elevation: 12,
78
+ shadowColor: '#000000',
79
+ shadowOpacity: 0.2,
80
+ shadowRadius: 12,
81
+ },
82
+ });
@@ -0,0 +1,127 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { Linking, StyleSheet, Text, View } from 'react-native';
3
+ import type { WebViewMessageEvent } from 'react-native-webview';
4
+
5
+ import type { WidgetProof } from '../backend/backend-transport.ts';
6
+ import type { MessageMetadata } from '../backend/message-metadata.ts';
7
+ import type { UnknownRecord } from '../fiber/narrow.ts';
8
+ import { buildEventScript, buildResolveScript, parsePageMessage, type PageMessage } from './bridge.ts';
9
+ import type { SessionRequest } from './chat-coordinator.ts';
10
+ import { type ChatNavigation, leavesChat } from './navigation.ts';
11
+ import { buildWidgetShell, type WidgetShellOptions } from './widget-shell.ts';
12
+ import { loadWebView, type WebViewHandle } from './webview.ts';
13
+
14
+ export interface ChatViewProps {
15
+ shell: WidgetShellOptions;
16
+ unavailableText: string;
17
+ getMetadata: (options: { includeTextDom: boolean }) => Promise<MessageMetadata | null>;
18
+ getProof: () => Promise<WidgetProof | null>;
19
+ onClosed: () => void;
20
+ onSessionChanged: (sessionId: number | null) => void;
21
+ open: boolean;
22
+ sessionRequest: SessionRequest | null;
23
+ log?: (message: string) => void;
24
+ }
25
+
26
+ const WebView = loadWebView();
27
+
28
+ export function ChatView({ shell, unavailableText, getMetadata, getProof, onClosed, onSessionChanged, open, sessionRequest, log }: ChatViewProps): React.JSX.Element {
29
+ const handle = useRef<WebViewHandle | null>(null);
30
+ const html = useMemo(() => buildWidgetShell(shell), [shell]);
31
+
32
+ const answer = useCallback((id: string, value: unknown, error: string | null) => {
33
+ handle.current?.injectJavaScript(buildResolveScript(id, value, error));
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ if (open) {
38
+ handle.current?.injectJavaScript(buildEventScript('open', null));
39
+ }
40
+ }, [open]);
41
+
42
+ useEffect(() => {
43
+ if (sessionRequest !== null) {
44
+ handle.current?.injectJavaScript(buildEventScript('openSession', { sessionId: sessionRequest.sessionId }));
45
+ }
46
+ }, [sessionRequest]);
47
+
48
+ const handleCall = useCallback(
49
+ async (message: Extract<PageMessage, { kind: 'call' }>) => {
50
+ try {
51
+ const value = message.name === 'metadata' ? await getMetadata({ includeTextDom: readIncludeTextDom(message.args) }) : await getProof();
52
+ answer(message.id, value, null);
53
+ } catch (error) {
54
+ answer(message.id, null, error instanceof Error ? error.message : String(error));
55
+ }
56
+ },
57
+ [answer, getMetadata, getProof],
58
+ );
59
+
60
+ const onMessage = useCallback(
61
+ (event: WebViewMessageEvent) => {
62
+ const message = parsePageMessage(event.nativeEvent.data);
63
+ if (message === null) {
64
+ return;
65
+ }
66
+ if (message.kind === 'call') {
67
+ void handleCall(message);
68
+ } else if (message.kind === 'closed') {
69
+ onClosed();
70
+ } else if (message.kind === 'session') {
71
+ onSessionChanged(message.sessionId);
72
+ } else if (message.kind === 'log') {
73
+ log?.(`webview ${message.level}: ${message.text}`);
74
+ } else {
75
+ log?.('webview ready');
76
+ }
77
+ },
78
+ [handleCall, log, onClosed, onSessionChanged],
79
+ );
80
+
81
+ const onNavigation = useCallback(
82
+ (navigation: ChatNavigation) => {
83
+ if (!leavesChat(navigation, shell.apiBaseUrl)) {
84
+ return true;
85
+ }
86
+ Linking.openURL(navigation.url).catch(() => log?.('chat link could not be opened'));
87
+ return false;
88
+ },
89
+ [log, shell.apiBaseUrl],
90
+ );
91
+
92
+ if (WebView === null) {
93
+ return (
94
+ <View style={styles.fallback}>
95
+ <Text style={styles.fallbackText}>{unavailableText}</Text>
96
+ </View>
97
+ );
98
+ }
99
+ return (
100
+ <WebView
101
+ ref={handle}
102
+ style={styles.webview}
103
+ originWhitelist={['*']}
104
+ source={{ html, baseUrl: shell.apiBaseUrl }}
105
+ javaScriptEnabled
106
+ domStorageEnabled
107
+ mixedContentMode="always"
108
+ setSupportMultipleWindows={false}
109
+ keyboardDisplayRequiresUserAction={false}
110
+ automaticallyAdjustContentInsets={false}
111
+ overScrollMode="never"
112
+ onMessage={onMessage}
113
+ onShouldStartLoadWithRequest={onNavigation}
114
+ onError={(event) => log?.(`webview load error: ${event.nativeEvent.description}`)}
115
+ />
116
+ );
117
+ }
118
+
119
+ function readIncludeTextDom(args: UnknownRecord): boolean {
120
+ return args.includeTextDom === true;
121
+ }
122
+
123
+ const styles = StyleSheet.create({
124
+ webview: { flex: 1, backgroundColor: 'transparent' },
125
+ fallback: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
126
+ fallbackText: { color: '#6b7280', textAlign: 'center' },
127
+ });
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
3
+
4
+ import { useInsets } from '../ui/useInsets.ts';
5
+
6
+ export interface LauncherPlacement {
7
+ side: 'left' | 'right';
8
+ offsetX: number;
9
+ offsetY: number;
10
+ }
11
+
12
+ export interface LauncherProps {
13
+ unread: number;
14
+ label: string;
15
+ color: string;
16
+ placement: LauncherPlacement;
17
+ onPress: () => void;
18
+ }
19
+
20
+ const SIZE = 56;
21
+
22
+ export function Launcher({ unread, label, color, placement, onPress }: LauncherProps): React.JSX.Element {
23
+ const insets = useInsets();
24
+ const horizontal = placement.side === 'left' ? { left: placement.offsetX } : { right: placement.offsetX };
25
+ return (
26
+ <Pressable
27
+ accessibilityRole="button"
28
+ accessibilityLabel={label}
29
+ onPress={onPress}
30
+ style={[styles.button, horizontal, { bottom: insets.bottom + placement.offsetY, backgroundColor: color }]}
31
+ >
32
+ <View style={styles.bubble}>
33
+ <View style={styles.tail} />
34
+ </View>
35
+ {unread > 0 ? (
36
+ <View style={styles.badge}>
37
+ <Text style={styles.badgeText}>{unread > 9 ? '9+' : String(unread)}</Text>
38
+ </View>
39
+ ) : null}
40
+ </Pressable>
41
+ );
42
+ }
43
+
44
+ const styles = StyleSheet.create({
45
+ button: {
46
+ position: 'absolute',
47
+ width: SIZE,
48
+ height: SIZE,
49
+ borderRadius: SIZE / 2,
50
+ alignItems: 'center',
51
+ justifyContent: 'center',
52
+ elevation: 6,
53
+ shadowColor: '#000000',
54
+ shadowOpacity: 0.25,
55
+ shadowRadius: 6,
56
+ },
57
+ bubble: { width: 26, height: 20, borderRadius: 8, backgroundColor: '#ffffff' },
58
+ tail: { position: 'absolute', left: 4, bottom: -5, width: 0, height: 0, borderLeftWidth: 5, borderRightWidth: 5, borderTopWidth: 7, borderLeftColor: 'transparent', borderRightColor: 'transparent', borderTopColor: '#ffffff' },
59
+ badge: {
60
+ position: 'absolute',
61
+ top: -2,
62
+ right: -2,
63
+ minWidth: 20,
64
+ height: 20,
65
+ paddingHorizontal: 5,
66
+ borderRadius: 10,
67
+ backgroundColor: '#ef4444',
68
+ alignItems: 'center',
69
+ justifyContent: 'center',
70
+ },
71
+ badgeText: { color: '#ffffff', fontSize: 11, fontWeight: '700' },
72
+ });
@@ -0,0 +1,56 @@
1
+ import { isRecord, nonEmptyString, type UnknownRecord } from '../fiber/narrow.ts';
2
+
3
+ export type NativeCallName = 'metadata' | 'proof';
4
+
5
+ export type PageMessage =
6
+ | { kind: 'ready' }
7
+ | { kind: 'closed' }
8
+ | { kind: 'session'; sessionId: number | null }
9
+ | { kind: 'call'; id: string; name: NativeCallName; args: UnknownRecord }
10
+ | { kind: 'log'; level: string; text: string };
11
+
12
+ const CALL_NAMES: readonly NativeCallName[] = ['metadata', 'proof'];
13
+
14
+ function isCallName(value: unknown): value is NativeCallName {
15
+ return CALL_NAMES.some((name) => name === value);
16
+ }
17
+
18
+ export function parsePageMessage(raw: string): PageMessage | null {
19
+ let parsed: unknown;
20
+ try {
21
+ parsed = JSON.parse(raw);
22
+ } catch {
23
+ return null;
24
+ }
25
+ if (!isRecord(parsed)) {
26
+ return null;
27
+ }
28
+ switch (parsed.kind) {
29
+ case 'ready':
30
+ return { kind: 'ready' };
31
+ case 'closed':
32
+ return { kind: 'closed' };
33
+ case 'session':
34
+ return typeof parsed.sessionId === 'number' || parsed.sessionId === null ? { kind: 'session', sessionId: parsed.sessionId } : null;
35
+ case 'log':
36
+ return typeof parsed.text === 'string' ? { kind: 'log', level: nonEmptyString(parsed.level) ?? 'log', text: parsed.text } : null;
37
+ case 'call': {
38
+ const id = nonEmptyString(parsed.id);
39
+ return id !== null && isCallName(parsed.name) ? { kind: 'call', id, name: parsed.name, args: isRecord(parsed.args) ? parsed.args : {} } : null;
40
+ }
41
+ default:
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function safeJson(value: unknown): string {
47
+ return JSON.stringify(value ?? null).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
48
+ }
49
+
50
+ export function buildResolveScript(id: string, value: unknown, error: string | null): string {
51
+ return `window.__blarNative && window.__blarNative.resolve(${safeJson(id)}, ${safeJson(value)}, ${safeJson(error)}); true;`;
52
+ }
53
+
54
+ export function buildEventScript(name: string, payload: unknown): string {
55
+ return `window.__blarNative && window.__blarNative.event(${safeJson(name)}, ${safeJson(payload)}); true;`;
56
+ }