@mobileai/react-native 0.9.27 → 0.9.28

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 (61) hide show
  1. package/README.md +24 -11
  2. package/android/build.gradle +17 -0
  3. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayDialogRootViewGroup.kt +243 -0
  4. package/android/src/main/java/com/mobileai/overlay/FloatingOverlayView.kt +281 -87
  5. package/android/src/newarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +52 -17
  6. package/android/src/oldarch/com/mobileai/overlay/FloatingOverlayViewManager.kt +49 -2
  7. package/bin/generate-map.cjs +45 -6
  8. package/ios/Podfile +63 -0
  9. package/ios/Podfile.lock +2290 -0
  10. package/ios/Podfile.properties.json +4 -0
  11. package/ios/mobileaireactnative/AppDelegate.swift +69 -0
  12. package/ios/mobileaireactnative/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  13. package/ios/mobileaireactnative/Images.xcassets/Contents.json +6 -0
  14. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/Contents.json +21 -0
  15. package/ios/mobileaireactnative/Images.xcassets/SplashScreenLegacy.imageset/SplashScreenLegacy.png +0 -0
  16. package/ios/mobileaireactnative/Info.plist +55 -0
  17. package/ios/mobileaireactnative/PrivacyInfo.xcprivacy +48 -0
  18. package/ios/mobileaireactnative/SplashScreen.storyboard +47 -0
  19. package/ios/mobileaireactnative/Supporting/Expo.plist +6 -0
  20. package/ios/mobileaireactnative/mobileaireactnative-Bridging-Header.h +3 -0
  21. package/ios/mobileaireactnative.xcodeproj/project.pbxproj +547 -0
  22. package/ios/mobileaireactnative.xcodeproj/xcshareddata/xcschemes/mobileaireactnative.xcscheme +88 -0
  23. package/ios/mobileaireactnative.xcworkspace/contents.xcworkspacedata +10 -0
  24. package/lib/module/components/AIAgent.js +405 -168
  25. package/lib/module/components/AgentChatBar.js +250 -59
  26. package/lib/module/components/FloatingOverlayWrapper.js +68 -32
  27. package/lib/module/config/endpoints.js +22 -1
  28. package/lib/module/core/AgentRuntime.js +103 -1
  29. package/lib/module/core/FiberTreeWalker.js +98 -0
  30. package/lib/module/core/OutcomeVerifier.js +149 -0
  31. package/lib/module/core/systemPrompt.js +96 -25
  32. package/lib/module/providers/GeminiProvider.js +9 -3
  33. package/lib/module/services/telemetry/TelemetryService.js +21 -2
  34. package/lib/module/services/telemetry/TouchAutoCapture.js +45 -35
  35. package/lib/module/specs/FloatingOverlayNativeComponent.ts +7 -1
  36. package/lib/module/support/supportPrompt.js +22 -7
  37. package/lib/module/support/supportStyle.js +55 -0
  38. package/lib/module/support/types.js +2 -0
  39. package/lib/module/tools/typeTool.js +20 -0
  40. package/lib/module/utils/humanizeScreenName.js +49 -0
  41. package/lib/typescript/src/components/AIAgent.d.ts +6 -2
  42. package/lib/typescript/src/components/AgentChatBar.d.ts +15 -1
  43. package/lib/typescript/src/components/FloatingOverlayWrapper.d.ts +22 -10
  44. package/lib/typescript/src/config/endpoints.d.ts +4 -0
  45. package/lib/typescript/src/core/AgentRuntime.d.ts +9 -0
  46. package/lib/typescript/src/core/FiberTreeWalker.d.ts +12 -1
  47. package/lib/typescript/src/core/OutcomeVerifier.d.ts +46 -0
  48. package/lib/typescript/src/core/systemPrompt.d.ts +3 -10
  49. package/lib/typescript/src/core/types.d.ts +35 -0
  50. package/lib/typescript/src/index.d.ts +1 -0
  51. package/lib/typescript/src/services/telemetry/TelemetryService.d.ts +7 -1
  52. package/lib/typescript/src/services/telemetry/types.d.ts +1 -1
  53. package/lib/typescript/src/specs/FloatingOverlayNativeComponent.d.ts +5 -0
  54. package/lib/typescript/src/support/index.d.ts +1 -0
  55. package/lib/typescript/src/support/supportStyle.d.ts +9 -0
  56. package/lib/typescript/src/support/types.d.ts +3 -0
  57. package/lib/typescript/src/utils/humanizeScreenName.d.ts +6 -0
  58. package/package.json +5 -2
  59. package/src/specs/FloatingOverlayNativeComponent.ts +7 -1
  60. package/ios/MobileAIFloatingOverlayComponentView.mm +0 -73
  61. package/ios/MobileAIPilotIntents.swift +0 -51
package/README.md CHANGED
@@ -391,6 +391,8 @@ npx @mobileai/react-native generate-map
391
391
 
392
392
  ### 2. Wrap Your App
393
393
 
394
+ If you use a MobileAI publishable key, the SDK now defaults to the hosted MobileAI text and voice proxies automatically. You only need to pass `proxyUrl` and `voiceProxyUrl` when you want to override them with your own backend.
395
+
394
396
  #### React Navigation
395
397
 
396
398
  ```tsx
@@ -403,13 +405,10 @@ export default function App() {
403
405
 
404
406
  return (
405
407
  <AIAgent
406
- // Your MobileAI Dashboard ID — instantly enables cloud intelligence
408
+ // Your MobileAI Dashboard ID
409
+ // This now auto-configures the hosted MobileAI text + voice proxies too.
407
410
  analyticsKey="mobileai_pub_xxxxxxxx"
408
411
 
409
- // Route all traffic through the secure MobileAI Cloud proxies
410
- proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
411
- voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
412
-
413
412
  navRef={navRef}
414
413
  screenMap={screenMap} // optional but recommended
415
414
  >
@@ -435,9 +434,8 @@ export default function RootLayout() {
435
434
 
436
435
  return (
437
436
  <AIAgent
437
+ // Hosted MobileAI proxies are inferred automatically from analyticsKey
438
438
  analyticsKey="mobileai_pub_xxxxxxxx"
439
- proxyUrl="https://mobileai.cloud/api/v1/hosted-proxy/text"
440
- voiceProxyUrl="wss://mobileai.cloud/ws/hosted-proxy/voice"
441
439
  navRef={navRef}
442
440
  screenMap={screenMap}
443
441
  >
@@ -464,6 +462,19 @@ The examples above use **Gemini** (default). To use **OpenAI** for text mode, ad
464
462
 
465
463
  A floating chat bar appears automatically. Ask the AI to navigate, tap buttons, fill forms, answer questions.
466
464
 
465
+ ### Hosted MobileAI Defaults
466
+
467
+ For the standard MobileAI Cloud setup, this is enough:
468
+
469
+ ```tsx
470
+ <AIAgent analyticsKey="mobileai_pub_xxxxxxxx" navRef={navRef} />
471
+ ```
472
+
473
+ Only pass explicit proxy props when:
474
+ - you want to use your own backend proxy
475
+ - you want a dedicated voice proxy
476
+ - you are self-hosting the MobileAI backend
477
+
467
478
  ### Knowledge-Only Mode — AI Assistant Without UI Automation
468
479
 
469
480
  Set `enableUIControl={false}` for a lightweight FAQ / support assistant. Single LLM call, ~70% fewer tokens:
@@ -871,9 +882,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
871
882
  |------|------|---------|-------------|
872
883
  | `apiKey` | `string` | — | API key for your provider (prototyping only — use `proxyUrl` in production). |
873
884
  | `provider` | `'gemini' \| 'openai'` | `'gemini'` | LLM provider for text mode. |
874
- | `proxyUrl` | `string` | | Backend proxy URL (production). Routes all LLM traffic through your server. |
885
+ | `proxyUrl` | `string` | Hosted MobileAI text proxy when `analyticsKey` is set | Backend proxy URL (production). Routes all LLM traffic through your server. |
875
886
  | `proxyHeaders` | `Record<string, string>` | — | Auth headers for proxy (e.g., `Authorization: Bearer ${token}`). |
876
- | `voiceProxyUrl` | `string` | | Dedicated proxy for Voice Mode WebSockets. Falls back to `proxyUrl`. |
887
+ | `voiceProxyUrl` | `string` | Hosted MobileAI voice proxy when `analyticsKey` is set; otherwise falls back to `proxyUrl` | Dedicated proxy for Voice Mode WebSockets. |
877
888
  | `voiceProxyHeaders` | `Record<string, string>` | — | Auth headers for voice proxy. |
878
889
  | `model` | `string` | Provider default | Model name (e.g. `gemini-2.5-flash`, `gpt-4.1-mini`). |
879
890
  | `navRef` | `NavigationContainerRef` | — | Navigation ref for auto-navigation. |
@@ -953,7 +964,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
953
964
 
954
965
  | Prop | Type | Default | Description |
955
966
  |------|------|---------|-------------|
956
- | `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics. |
967
+ | `analyticsKey` | `string` | — | Publishable key (`mobileai_pub_xxx`) — enables auto-analytics and, by default, the hosted MobileAI text/voice proxies. |
957
968
  | `analyticsProxyUrl` | `string` | — | Enterprise: route events through your backend. |
958
969
  | `analyticsProxyHeaders` | `Record<string, string>` | — | Auth headers for analytics proxy. |
959
970
 
@@ -1065,7 +1076,7 @@ const { send } = useAI({
1065
1076
 
1066
1077
  ## 📊 Zero-Config Analytics — Auto-Capture Every Tap
1067
1078
 
1068
- Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. **Zero code changes** to your app components.
1079
+ Just add `analyticsKey` — every button tap, screen navigation, and session is tracked automatically. It also enables the default hosted MobileAI AI proxies unless you override them. **Zero code changes** to your app components.
1069
1080
 
1070
1081
  ```tsx
1071
1082
  <AIAgent
@@ -1116,6 +1127,8 @@ MobileAI.identify('user_123', { plan: 'pro' });
1116
1127
 
1117
1128
  ### Backend Proxy — Keep API Keys Secure
1118
1129
 
1130
+ Use this only when you want to override the default hosted MobileAI proxy behavior or route traffic through your own backend.
1131
+
1119
1132
  ```tsx
1120
1133
  <AIAgent
1121
1134
  proxyUrl="https://myapp.vercel.app/api/gemini"
@@ -10,6 +10,15 @@ buildscript {
10
10
  }
11
11
  }
12
12
 
13
+ def isNewArchitectureEnabled() {
14
+ // Libraries opt into Fabric/TurboModule codegen only when the consuming app enables it.
15
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
16
+ }
17
+
18
+ if (isNewArchitectureEnabled()) {
19
+ apply plugin: "com.facebook.react"
20
+ }
21
+
13
22
  apply plugin: "com.android.library"
14
23
  apply plugin: "kotlin-android"
15
24
 
@@ -59,3 +68,11 @@ repositories {
59
68
  dependencies {
60
69
  implementation "com.facebook.react:react-android"
61
70
  }
71
+
72
+ if (isNewArchitectureEnabled()) {
73
+ react {
74
+ jsRootDir = file("../")
75
+ libraryName = "RNMobileAIOverlaySpec"
76
+ codegenJavaPackageName = "com.mobileai.overlay"
77
+ }
78
+ }
@@ -0,0 +1,243 @@
1
+ package com.mobileai.overlay
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.view.MotionEvent
6
+ import android.view.View
7
+ import android.view.ViewConfiguration
8
+ import com.facebook.react.bridge.GuardedRunnable
9
+ import com.facebook.react.bridge.WritableMap
10
+ import com.facebook.react.bridge.WritableNativeMap
11
+ import com.facebook.react.common.annotations.UnstableReactNativeAPI
12
+ import com.facebook.react.common.build.ReactBuildConfig
13
+ import com.facebook.react.config.ReactFeatureFlags
14
+ import com.facebook.react.uimanager.JSPointerDispatcher
15
+ import com.facebook.react.uimanager.JSTouchDispatcher
16
+ import com.facebook.react.uimanager.PixelUtil.pxToDp
17
+ import com.facebook.react.uimanager.RootView
18
+ import com.facebook.react.uimanager.StateWrapper
19
+ import com.facebook.react.uimanager.ThemedReactContext
20
+ import com.facebook.react.uimanager.UIManagerModule
21
+ import com.facebook.react.uimanager.events.EventDispatcher
22
+ import com.facebook.react.views.view.ReactViewGroup
23
+ import kotlin.math.abs
24
+ import kotlin.math.roundToInt
25
+
26
+ /**
27
+ * Root view rendered inside the Android floating panel dialog.
28
+ *
29
+ * This mirrors the touch and layout behavior of React Native's modal dialog
30
+ * host so JS children remain fully interactive even though they render inside
31
+ * a separate native window.
32
+ */
33
+ internal class FloatingOverlayDialogRootViewGroup(context: Context) :
34
+ ReactViewGroup(context),
35
+ RootView {
36
+
37
+ var stateWrapper: StateWrapper? = null
38
+ var eventDispatcher: EventDispatcher? = null
39
+ var overlayHost: FloatingOverlayView? = null
40
+
41
+ private var viewWidth: Int = 0
42
+ private var viewHeight: Int = 0
43
+ private val jsTouchDispatcher = JSTouchDispatcher(this)
44
+ private var jsPointerDispatcher: JSPointerDispatcher? = null
45
+ private val touchSlop: Float = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
46
+ private var dragCandidate: Boolean = false
47
+ private var isDraggingWindow: Boolean = false
48
+ private var dragStartRawX: Float = 0f
49
+ private var dragStartRawY: Float = 0f
50
+ private var dragStartWindowX: Int = 0
51
+ private var dragStartWindowY: Int = 0
52
+
53
+ private val reactContext: ThemedReactContext
54
+ get() = context as ThemedReactContext
55
+
56
+ init {
57
+ if (ReactFeatureFlags.dispatchPointerEvents) {
58
+ jsPointerDispatcher = JSPointerDispatcher(this)
59
+ }
60
+ }
61
+
62
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
63
+ super.onSizeChanged(w, h, oldw, oldh)
64
+ viewWidth = w
65
+ viewHeight = h
66
+ updateState(viewWidth, viewHeight)
67
+ }
68
+
69
+ fun updateState(width: Int, height: Int) {
70
+ val realWidth = width.toFloat().pxToDp()
71
+ val realHeight = height.toFloat().pxToDp()
72
+
73
+ val wrapper = stateWrapper
74
+ if (wrapper != null) {
75
+ val newStateData: WritableMap = WritableNativeMap()
76
+ newStateData.putDouble("screenWidth", realWidth.toDouble())
77
+ newStateData.putDouble("screenHeight", realHeight.toDouble())
78
+ wrapper.updateState(newStateData)
79
+ } else if (
80
+ !ReactBuildConfig.UNSTABLE_ENABLE_MINIFY_LEGACY_ARCHITECTURE &&
81
+ !reactContext.isBridgeless
82
+ ) {
83
+ @Suppress("DEPRECATION")
84
+ reactContext.runOnNativeModulesQueueThread(
85
+ object : GuardedRunnable(reactContext) {
86
+ override fun runGuarded() {
87
+ reactContext.reactApplicationContext
88
+ .getNativeModule(UIManagerModule::class.java)
89
+ ?.updateNodeSize(id, viewWidth, viewHeight)
90
+ }
91
+ }
92
+ )
93
+ }
94
+ }
95
+
96
+ override fun handleException(t: Throwable) {
97
+ reactContext.reactApplicationContext.handleException(RuntimeException(t))
98
+ }
99
+
100
+ override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
101
+ when (event.actionMasked) {
102
+ MotionEvent.ACTION_DOWN -> {
103
+ dragCandidate = isInNativeDragRegion(event.x, event.y)
104
+ isDraggingWindow = false
105
+ if (dragCandidate) {
106
+ dragStartRawX = event.rawX
107
+ dragStartRawY = event.rawY
108
+ dragStartWindowX = overlayHost?.getWindowXPx() ?: 0
109
+ dragStartWindowY = overlayHost?.getWindowYPx() ?: 0
110
+ }
111
+ }
112
+
113
+ MotionEvent.ACTION_MOVE -> {
114
+ if (dragCandidate && !isDraggingWindow && hasExceededTouchSlop(event)) {
115
+ isDraggingWindow = true
116
+ parent?.requestDisallowInterceptTouchEvent(true)
117
+ updateNativeWindowDrag(event)
118
+ return true
119
+ }
120
+ }
121
+
122
+ MotionEvent.ACTION_UP,
123
+ MotionEvent.ACTION_CANCEL -> resetNativeDrag()
124
+ }
125
+
126
+ if (isDraggingWindow) return true
127
+
128
+ eventDispatcher?.let { dispatcher ->
129
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
130
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
131
+ }
132
+ return super.onInterceptTouchEvent(event)
133
+ }
134
+
135
+ @SuppressLint("ClickableViewAccessibility")
136
+ override fun onTouchEvent(event: MotionEvent): Boolean {
137
+ if (isDraggingWindow || dragCandidate) {
138
+ when (event.actionMasked) {
139
+ MotionEvent.ACTION_MOVE -> {
140
+ if (!isDraggingWindow && hasExceededTouchSlop(event)) {
141
+ isDraggingWindow = true
142
+ parent?.requestDisallowInterceptTouchEvent(true)
143
+ }
144
+
145
+ if (isDraggingWindow) {
146
+ updateNativeWindowDrag(event)
147
+ return true
148
+ }
149
+ }
150
+
151
+ MotionEvent.ACTION_UP -> {
152
+ if (isDraggingWindow) {
153
+ updateNativeWindowDrag(event)
154
+ overlayHost?.emitWindowDragEnd()
155
+ resetNativeDrag()
156
+ return true
157
+ }
158
+ resetNativeDrag()
159
+ }
160
+
161
+ MotionEvent.ACTION_CANCEL -> {
162
+ if (isDraggingWindow) {
163
+ overlayHost?.emitWindowDragEnd()
164
+ resetNativeDrag()
165
+ return true
166
+ }
167
+ resetNativeDrag()
168
+ }
169
+ }
170
+ }
171
+
172
+ eventDispatcher?.let { dispatcher ->
173
+ jsTouchDispatcher.handleTouchEvent(event, dispatcher, reactContext)
174
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
175
+ }
176
+ super.onTouchEvent(event)
177
+ return true
178
+ }
179
+
180
+ override fun onInterceptHoverEvent(event: MotionEvent): Boolean {
181
+ eventDispatcher?.let { dispatcher ->
182
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, true)
183
+ }
184
+ return super.onInterceptHoverEvent(event)
185
+ }
186
+
187
+ override fun onHoverEvent(event: MotionEvent): Boolean {
188
+ eventDispatcher?.let { dispatcher ->
189
+ jsPointerDispatcher?.handleMotionEvent(event, dispatcher, false)
190
+ }
191
+ return super.onHoverEvent(event)
192
+ }
193
+
194
+ @OptIn(UnstableReactNativeAPI::class)
195
+ override fun onChildStartedNativeGesture(childView: View?, ev: MotionEvent) {
196
+ eventDispatcher?.let { dispatcher ->
197
+ jsTouchDispatcher.onChildStartedNativeGesture(ev, dispatcher, reactContext)
198
+ jsPointerDispatcher?.onChildStartedNativeGesture(childView, ev, dispatcher)
199
+ }
200
+ }
201
+
202
+ override fun onChildEndedNativeGesture(childView: View, ev: MotionEvent) {
203
+ eventDispatcher?.let { dispatcher ->
204
+ jsTouchDispatcher.onChildEndedNativeGesture(ev, dispatcher)
205
+ }
206
+ jsPointerDispatcher?.onChildEndedNativeGesture()
207
+ }
208
+
209
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
210
+ // No-op so the root can continue receiving events for JS dispatch.
211
+ }
212
+
213
+ private fun hasExceededTouchSlop(event: MotionEvent): Boolean {
214
+ return abs(event.rawX - dragStartRawX) > touchSlop || abs(event.rawY - dragStartRawY) > touchSlop
215
+ }
216
+
217
+ private fun updateNativeWindowDrag(event: MotionEvent) {
218
+ val targetX = dragStartWindowX + (event.rawX - dragStartRawX).roundToInt()
219
+ val targetY = dragStartWindowY + (event.rawY - dragStartRawY).roundToInt()
220
+ overlayHost?.updateWindowPositionFromNative(targetX, targetY)
221
+ }
222
+
223
+ private fun resetNativeDrag() {
224
+ dragCandidate = false
225
+ isDraggingWindow = false
226
+ }
227
+
228
+ private fun isInNativeDragRegion(x: Float, y: Float): Boolean {
229
+ if (viewWidth <= dpToPx(80) && viewHeight <= dpToPx(80)) {
230
+ return true
231
+ }
232
+
233
+ val handleTop = dpToPx(44)
234
+ val handleHalfWidth = dpToPx(72)
235
+ val centerX = viewWidth / 2f
236
+
237
+ return y <= handleTop && x in (centerX - handleHalfWidth)..(centerX + handleHalfWidth)
238
+ }
239
+
240
+ private fun dpToPx(value: Int): Float {
241
+ return value * resources.displayMetrics.density
242
+ }
243
+ }