@ateam-ai-mobile/expo 0.1.4 → 0.1.7

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 (158) hide show
  1. package/android/build.gradle +16 -0
  2. package/android/src/main/java/expo/modules/adavision/AdaVisionModule.kt +75 -0
  3. package/dist/attachmentCache-DhQTqi2E.d.mts +18 -0
  4. package/dist/attachmentCache-DhQTqi2E.d.ts +18 -0
  5. package/dist/bootstrap.d.mts +95 -0
  6. package/dist/bootstrap.d.ts +95 -0
  7. package/dist/bootstrap.js +2473 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/bootstrap.mjs +64 -0
  10. package/dist/bootstrap.mjs.map +1 -0
  11. package/dist/chunk-4BMWVN25.mjs +91 -0
  12. package/dist/chunk-4BMWVN25.mjs.map +1 -0
  13. package/dist/chunk-4XYXJYN5.mjs +1513 -0
  14. package/dist/chunk-4XYXJYN5.mjs.map +1 -0
  15. package/dist/chunk-5HM6UA2K.mjs +571 -0
  16. package/dist/chunk-5HM6UA2K.mjs.map +1 -0
  17. package/dist/chunk-6LXF3JVX.mjs +96 -0
  18. package/dist/chunk-6LXF3JVX.mjs.map +1 -0
  19. package/dist/chunk-FF6ZZZXH.mjs +71 -0
  20. package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
  21. package/dist/chunk-KOXF2FZM.mjs +92 -0
  22. package/dist/chunk-KOXF2FZM.mjs.map +1 -0
  23. package/dist/chunk-L4V7OCJ3.mjs +71 -0
  24. package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
  25. package/dist/chunk-TZXWVYYR.mjs +67 -0
  26. package/dist/chunk-TZXWVYYR.mjs.map +1 -0
  27. package/dist/chunk-VQJPSLHP.mjs +1801 -0
  28. package/dist/chunk-VQJPSLHP.mjs.map +1 -0
  29. package/dist/chunk-WMJKH4XE.mjs +34 -0
  30. package/dist/chunk-WMJKH4XE.mjs.map +1 -0
  31. package/dist/chunk-XQSY6MHP.mjs +213 -0
  32. package/dist/chunk-XQSY6MHP.mjs.map +1 -0
  33. package/dist/createBridge.mjs +2 -0
  34. package/dist/createBridge.mjs.map +1 -1
  35. package/dist/device/createDeviceProvider.d.mts +23 -0
  36. package/dist/device/createDeviceProvider.d.ts +23 -0
  37. package/dist/device/createDeviceProvider.js +967 -0
  38. package/dist/device/createDeviceProvider.js.map +1 -0
  39. package/dist/device/createDeviceProvider.mjs +17 -0
  40. package/dist/device/createDeviceProvider.mjs.map +1 -0
  41. package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
  42. package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
  43. package/dist/device-bridge/collectors/appEngagement.js +128 -0
  44. package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
  45. package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
  46. package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
  47. package/dist/device-bridge/collectors/health.d.mts +24 -0
  48. package/dist/device-bridge/collectors/health.d.ts +24 -0
  49. package/dist/device-bridge/collectors/health.js +238 -0
  50. package/dist/device-bridge/collectors/health.js.map +1 -0
  51. package/dist/device-bridge/collectors/health.mjs +18 -0
  52. package/dist/device-bridge/collectors/health.mjs.map +1 -0
  53. package/dist/device-bridge/cookies.d.mts +49 -0
  54. package/dist/device-bridge/cookies.d.ts +49 -0
  55. package/dist/device-bridge/cookies.js +90 -0
  56. package/dist/device-bridge/cookies.js.map +1 -0
  57. package/dist/device-bridge/cookies.mjs +14 -0
  58. package/dist/device-bridge/cookies.mjs.map +1 -0
  59. package/dist/device-bridge/index.d.mts +483 -0
  60. package/dist/device-bridge/index.d.ts +483 -0
  61. package/dist/device-bridge/index.js +2084 -0
  62. package/dist/device-bridge/index.js.map +1 -0
  63. package/dist/device-bridge/index.mjs +32 -0
  64. package/dist/device-bridge/index.mjs.map +1 -0
  65. package/dist/device-bridge/nfc.d.mts +30 -0
  66. package/dist/device-bridge/nfc.d.ts +30 -0
  67. package/dist/device-bridge/nfc.js +109 -0
  68. package/dist/device-bridge/nfc.js.map +1 -0
  69. package/dist/device-bridge/nfc.mjs +14 -0
  70. package/dist/device-bridge/nfc.mjs.map +1 -0
  71. package/dist/index.d.mts +493 -18
  72. package/dist/index.d.ts +493 -18
  73. package/dist/index.js +1409 -6
  74. package/dist/index.js.map +1 -1
  75. package/dist/index.mjs +23 -194
  76. package/dist/index.mjs.map +1 -1
  77. package/dist/oauth/expoOAuth.d.mts +18 -0
  78. package/dist/oauth/expoOAuth.d.ts +18 -0
  79. package/dist/oauth/expoOAuth.js +102 -0
  80. package/dist/oauth/expoOAuth.js.map +1 -0
  81. package/dist/oauth/expoOAuth.mjs +8 -0
  82. package/dist/oauth/expoOAuth.mjs.map +1 -0
  83. package/dist/types-BRanJYTn.d.mts +138 -0
  84. package/dist/types-BRanJYTn.d.ts +138 -0
  85. package/dist/vision/index.d.mts +81 -0
  86. package/dist/vision/index.d.ts +81 -0
  87. package/dist/vision/index.js +101 -0
  88. package/dist/vision/index.js.map +1 -0
  89. package/dist/vision/index.mjs +28 -0
  90. package/dist/vision/index.mjs.map +1 -0
  91. package/expo-module.config.json +9 -0
  92. package/ios/AdaVision.podspec +28 -0
  93. package/ios/AdaVisionModule.swift +432 -0
  94. package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
  95. package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
  96. package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
  97. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
  98. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
  99. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
  100. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
  101. package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
  102. package/package.json +107 -7
  103. package/src/bootstrap.ts +159 -0
  104. package/src/camera/CameraTaskHost.tsx +175 -0
  105. package/src/camera/cameraTasks.ts +129 -0
  106. package/src/device/createDeviceProvider.ts +832 -0
  107. package/src/device-bridge/actions/calendar-actions.ts +217 -0
  108. package/src/device-bridge/actions/dnd.ts +74 -0
  109. package/src/device-bridge/actions/health-actions.ts +58 -0
  110. package/src/device-bridge/actions/index.ts +12 -0
  111. package/src/device-bridge/actions/messaging.ts +74 -0
  112. package/src/device-bridge/actions/navigation.ts +88 -0
  113. package/src/device-bridge/actions/queue-poller.ts +151 -0
  114. package/src/device-bridge/background.ts +137 -0
  115. package/src/device-bridge/biometrics.ts +64 -0
  116. package/src/device-bridge/capabilities.ts +218 -0
  117. package/src/device-bridge/collectors/appEngagement.ts +120 -0
  118. package/src/device-bridge/collectors/battery.ts +83 -0
  119. package/src/device-bridge/collectors/calendar.ts +162 -0
  120. package/src/device-bridge/collectors/camera.ts +27 -0
  121. package/src/device-bridge/collectors/connectivity.ts +60 -0
  122. package/src/device-bridge/collectors/contacts.ts +200 -0
  123. package/src/device-bridge/collectors/health.ts +299 -0
  124. package/src/device-bridge/collectors/index.ts +12 -0
  125. package/src/device-bridge/collectors/location.ts +224 -0
  126. package/src/device-bridge/collectors/mood.ts +141 -0
  127. package/src/device-bridge/collectors/notifications.ts +70 -0
  128. package/src/device-bridge/cookies.ts +89 -0
  129. package/src/device-bridge/dynamic-connector.ts +422 -0
  130. package/src/device-bridge/index.ts +467 -0
  131. package/src/device-bridge/nfc.ts +85 -0
  132. package/src/device-bridge/plugin.ts +71 -0
  133. package/src/device-bridge/relay-client.ts +237 -0
  134. package/src/device-bridge/scanner.ts +42 -0
  135. package/src/device-bridge/sharing.ts +35 -0
  136. package/src/device-bridge/types.ts +326 -0
  137. package/src/device-state/activityClassifier.ts +151 -0
  138. package/src/device-state/activityEngine.ts +252 -0
  139. package/src/device-state/collectors.ts +31 -0
  140. package/src/device-state/domains/activity.ts +93 -0
  141. package/src/device-state/domains/capability.ts +60 -0
  142. package/src/device-state/domains/connectivity.ts +60 -0
  143. package/src/device-state/domains/deviceContext.ts +55 -0
  144. package/src/device-state/domains/interaction.ts +59 -0
  145. package/src/device-state/domains/spatial.ts +283 -0
  146. package/src/device-state/domains/visual.ts +100 -0
  147. package/src/device-state/headingSource.ts +84 -0
  148. package/src/device-state/index.ts +77 -0
  149. package/src/device-state/quality.ts +60 -0
  150. package/src/device-state/replay.ts +220 -0
  151. package/src/device-state/turnDetector.ts +121 -0
  152. package/src/device-state/visualEngine.ts +341 -0
  153. package/src/haptics.ts +4 -0
  154. package/src/index.ts +37 -0
  155. package/src/oauth/expoOAuth.tsx +113 -0
  156. package/src/vision/analysis.js +80 -0
  157. package/src/vision/index.ts +144 -0
  158. package/src/vision/ring.js +35 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai-mobile/expo",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Expo-backed providers for the ADAS SDK — haptics, bundle disk cache, attachment cache, device bridge.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,29 +9,45 @@
9
9
  "build": "tsup"
10
10
  },
11
11
  "dependencies": {
12
- "@ateam-ai-mobile/core": "^0.1.4",
13
- "@ateam-ai-mobile/plugin-runtime": "^0.1.4",
14
- "@ateam-ai-mobile/ui": "^0.1.4"
12
+ "@ateam-ai-mobile/core": "0.1.7",
13
+ "@ateam-ai-mobile/plugin-runtime": "0.1.7",
14
+ "@ateam-ai-mobile/ui": "0.1.7"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@react-native-async-storage/async-storage": "*",
18
+ "@react-native-cookies/cookies": "*",
19
+ "@react-native-google-signin/google-signin": "*",
20
+ "expo": "*",
21
+ "expo-apple-authentication": "*",
22
+ "expo-background-fetch": "*",
18
23
  "expo-battery": "*",
19
24
  "expo-calendar": "*",
25
+ "expo-camera": "*",
26
+ "expo-clipboard": "*",
20
27
  "expo-contacts": "*",
21
28
  "expo-device": "*",
22
29
  "expo-file-system": "*",
23
30
  "expo-haptics": "*",
24
31
  "expo-linking": "*",
32
+ "expo-local-authentication": "*",
25
33
  "expo-location": "*",
34
+ "expo-modules-core": "*",
26
35
  "expo-network": "*",
27
36
  "expo-notifications": "*",
37
+ "expo-sharing": "*",
28
38
  "expo-sms": "*",
39
+ "expo-task-manager": "*",
29
40
  "expo-web-browser": "*",
30
41
  "react": ">=18",
31
- "react-native": "*",
32
- "expo-clipboard": "*"
42
+ "react-native": "*"
33
43
  },
34
44
  "peerDependenciesMeta": {
45
+ "@react-native-google-signin/google-signin": {
46
+ "optional": true
47
+ },
48
+ "expo-apple-authentication": {
49
+ "optional": true
50
+ },
35
51
  "expo-battery": {
36
52
  "optional": true
37
53
  },
@@ -55,6 +71,33 @@
55
71
  },
56
72
  "expo-sms": {
57
73
  "optional": true
74
+ },
75
+ "expo-camera": {
76
+ "optional": true
77
+ },
78
+ "expo-task-manager": {
79
+ "optional": true
80
+ },
81
+ "expo-background-fetch": {
82
+ "optional": true
83
+ },
84
+ "expo-local-authentication": {
85
+ "optional": true
86
+ },
87
+ "expo-sharing": {
88
+ "optional": true
89
+ },
90
+ "@react-native-cookies/cookies": {
91
+ "optional": true
92
+ },
93
+ "expo-linking": {
94
+ "optional": true
95
+ },
96
+ "expo": {
97
+ "optional": true
98
+ },
99
+ "expo-modules-core": {
100
+ "optional": true
58
101
  }
59
102
  },
60
103
  "exports": {
@@ -69,6 +112,42 @@
69
112
  "types": "./dist/createBridge.d.ts",
70
113
  "import": "./dist/createBridge.mjs",
71
114
  "require": "./dist/createBridge.js"
115
+ },
116
+ "./device-bridge": {
117
+ "react-native": "./src/device-bridge/index.ts",
118
+ "types": "./dist/device-bridge/index.d.ts",
119
+ "import": "./dist/device-bridge/index.mjs",
120
+ "require": "./dist/device-bridge/index.js"
121
+ },
122
+ "./device-bridge/*": {
123
+ "react-native": "./src/device-bridge/*.ts",
124
+ "types": "./dist/device-bridge/*.d.ts",
125
+ "import": "./dist/device-bridge/*.mjs",
126
+ "require": "./dist/device-bridge/*.js"
127
+ },
128
+ "./vision": {
129
+ "react-native": "./src/vision/index.ts",
130
+ "types": "./dist/vision/index.d.ts",
131
+ "import": "./dist/vision/index.mjs",
132
+ "require": "./dist/vision/index.js"
133
+ },
134
+ "./device": {
135
+ "react-native": "./src/device/createDeviceProvider.ts",
136
+ "types": "./dist/device/createDeviceProvider.d.ts",
137
+ "import": "./dist/device/createDeviceProvider.mjs",
138
+ "require": "./dist/device/createDeviceProvider.js"
139
+ },
140
+ "./oauth": {
141
+ "react-native": "./src/oauth/expoOAuth.tsx",
142
+ "types": "./dist/oauth/expoOAuth.d.ts",
143
+ "import": "./dist/oauth/expoOAuth.mjs",
144
+ "require": "./dist/oauth/expoOAuth.js"
145
+ },
146
+ "./bootstrap": {
147
+ "react-native": "./src/bootstrap.ts",
148
+ "types": "./dist/bootstrap.d.ts",
149
+ "import": "./dist/bootstrap.mjs",
150
+ "require": "./dist/bootstrap.js"
72
151
  }
73
152
  },
74
153
  "module": "dist/index.mjs",
@@ -76,9 +155,30 @@
76
155
  "react-native": "src/index.ts",
77
156
  "files": [
78
157
  "dist",
79
- "src"
158
+ "src",
159
+ "ios",
160
+ "android",
161
+ "expo-module.config.json"
80
162
  ],
81
163
  "publishConfig": {
82
164
  "access": "public"
165
+ },
166
+ "devDependencies": {
167
+ "@react-native-cookies/cookies": "*",
168
+ "@react-native-google-signin/google-signin": "*",
169
+ "expo-apple-authentication": "*",
170
+ "expo-background-fetch": "*",
171
+ "expo-battery": "*",
172
+ "expo-calendar": "*",
173
+ "expo-camera": "*",
174
+ "expo-contacts": "*",
175
+ "expo-local-authentication": "*",
176
+ "expo-location": "*",
177
+ "expo-network": "*",
178
+ "expo-notifications": "*",
179
+ "expo-sharing": "*",
180
+ "expo-sms": "*",
181
+ "expo-task-manager": "*",
182
+ "react-native-nfc-manager": "*"
83
183
  }
84
184
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * bootstrapExpoAdas — the single, self-contained SDK bootstrap (R10).
3
+ *
4
+ * The final high-level entry point of the "fat SDK / thin shell" arc: it folds
5
+ * the ~15 hand-wired seams the host used to assemble in its own adas.ts into ONE
6
+ * call, so the shell stops manually assembling platform providers/collectors/
7
+ * plugins. Internally it initializes, in the order the migrated code established:
8
+ * the Expo providers (haptics/bundle cache/clipboard/attachment cache + the
9
+ * device-primitive provider), the host-bundled native plugins, the plugin-WebView
10
+ * allow-list, native OAuth, the ACS reachability reporter, the L3 DeviceState
11
+ * engine (with the SDK's OWN collectors — the shell injects none), the plugin/
12
+ * UI-action navigator, and finally the core config+token bridge (initAdas).
13
+ *
14
+ * The API is DERIVED from the host's migrated adas.ts, not invented. Everything
15
+ * the host still owns is passed in as a small values-and-getters bag: endpoint
16
+ * getters (the host owns the dev/prod switch), the identity/token bridge, its
17
+ * branded Google client id, its host-bundled plugin resolvers, the native
18
+ * WebView module from its build, and its router callback.
19
+ *
20
+ * A SUBPATH (`@ateam-ai-mobile/expo/bootstrap`), like /device and /oauth: it
21
+ * statically pulls the optional expo native modules (via createDeviceProvider /
22
+ * the device-state collectors / the OAuth provider), so it must not sit on the
23
+ * package's main entry, which a host without those modules still has to build.
24
+ *
25
+ * The two physical reasoning paths stay DISTINCT and are wired as SEPARATE
26
+ * statements — never a merged bridge: LOCAL (the local tool registry + device
27
+ * state the plugin/voice runtime reads) and SERVER (the relay allow-list here;
28
+ * the DeviceBridge relay runtime itself is started by the host lifecycle, out of
29
+ * this module-load bootstrap's scope).
30
+ */
31
+ import { initAdas, setAcsReachabilityReporter, type TokenProvider, type AdasUser, type AdasSession } from '@ateam-ai-mobile/core';
32
+ import { setDeviceStateProvider, registerNativePlugin, registerPlatformDeviceTools, type RegisteredPlugin } from '@ateam-ai-mobile/plugin-runtime';
33
+ import { setOAuthProvider, setUIActionNavigator, setPluginWebViewExtraHosts, type UIActionRoute } from '@ateam-ai-mobile/ui';
34
+
35
+ import { setupExpoProviders, type ExpoAttachmentCache } from './index';
36
+ import { createDeviceProvider } from './device/createDeviceProvider';
37
+ import { createDeviceState } from './device-state';
38
+ import { createExpoOAuthProvider } from './oauth/expoOAuth';
39
+ import * as deviceConnectivity from './device-bridge/collectors/connectivity';
40
+ import * as deviceLocation from './device-bridge/collectors/location';
41
+
42
+ /** A host-bundled native plugin: one lazy resolver registered under one or more ids. */
43
+ export interface NativePluginRegistration {
44
+ resolver: () => Promise<RegisteredPlugin>;
45
+ ids: string[];
46
+ }
47
+
48
+ export interface BootstrapExpoAdasOptions {
49
+ /**
50
+ * Endpoint GETTERS — the host owns the dev/prod env switch, so these are read
51
+ * live on every SDK request (never snapshotted). `core` is required; the rest
52
+ * are optional (voice/consumer-auth features light up when present).
53
+ */
54
+ urls: {
55
+ core: () => string;
56
+ voice?: () => string;
57
+ consumerAuth?: () => string;
58
+ /** Trusted extra host for plugin WebViews (the relay device-bridge auth flows). */
59
+ relay?: () => string;
60
+ };
61
+ /** The host's token bridge — outcome mapping (transient vs auth_failed) owned by the host. */
62
+ tokenProvider: TokenProvider;
63
+ /** Current user projection for chat routing (host reads its auth store). */
64
+ getUser: () => AdasUser | null;
65
+ /** Active tenant getter (signed-in tenant, falling back to the pre-auth pick). */
66
+ tenant?: () => string;
67
+ /** Adopt a freshly-minted session on tenant switch (host persists tenant-first, then logs in). */
68
+ onSession?: (session: AdasSession) => void | Promise<void>;
69
+ /** The host's bundled Google iOS client id, for the join/open-join OAuth path. */
70
+ googleIosClientId?: string;
71
+ /** Host-bundled native plugins (AuthWebView, ConnectionsPanel, …) — lazy resolvers. */
72
+ nativePlugins?: NativePluginRegistration[];
73
+ /** Native modules remote bundles may require() (e.g. react-native-webview) — from the host build. */
74
+ bundleModules?: Record<string, unknown>;
75
+ /** Pre-SDK AsyncStorage keys, so an existing install keeps its warm caches. */
76
+ storageKeys?: { attachmentIndex?: string; bundleIndex?: string };
77
+ /** Connectivity sink — the SDK reports fetch reachability up/down; the host feeds its banner. */
78
+ onReachabilityChange?: (up: boolean) => void;
79
+ /** Open the host's plugin screen for a UI-action chip (the host owns its router). */
80
+ navigateToPlugin: (route: UIActionRoute) => void;
81
+ /**
82
+ * Reachability probe URL for the device-primitive connectivity API. The host
83
+ * passes this EXPLICITLY (typically `${core}/health`) as a load-time snapshot —
84
+ * deliberately NOT derived from `urls.core()`, so a dev/E2E core override does
85
+ * not silently flow into the health probe (the asymmetry the host relied on).
86
+ */
87
+ reachabilityUrl?: string;
88
+ }
89
+
90
+ /**
91
+ * Wire the entire Expo-backed ADAS platform in one call. Run for its side effect
92
+ * at module load, before the router mounts. Returns the attachment cache so the
93
+ * host can hand it to anything that needs it (initAdas already received it).
94
+ */
95
+ export function bootstrapExpoAdas(opts: BootstrapExpoAdasOptions): { attachmentCache: ExpoAttachmentCache } {
96
+ // Expo providers + the device-primitive implementation (props.device). The SDK
97
+ // owns createDeviceProvider; the shell injects only its reachability URL.
98
+ const { attachmentCache } = setupExpoProviders({
99
+ bundleModules: opts.bundleModules,
100
+ attachmentIndexKey: opts.storageKeys?.attachmentIndex,
101
+ bundleIndexKey: opts.storageKeys?.bundleIndex,
102
+ deviceProvider: createDeviceProvider({ reachabilityUrl: opts.reachabilityUrl }),
103
+ });
104
+
105
+ // Host-bundled native plugins: register each lazy resolver under its id aliases.
106
+ for (const plugin of opts.nativePlugins ?? []) {
107
+ for (const id of plugin.ids) registerNativePlugin(id, plugin.resolver);
108
+ }
109
+
110
+ // Plugin WebViews may navigate to the relay (device-bridge auth flows) —
111
+ // whitelist it alongside core/voice. Getter → live across env switches.
112
+ const relay = opts.urls.relay;
113
+ setPluginWebViewExtraHosts(() => (relay ? [relay()] : []));
114
+
115
+ // Native OAuth for the relocated auth screens (the ONLY native OAuth wiring).
116
+ setOAuthProvider(createExpoOAuthProvider({ googleIosClientId: opts.googleIosClientId }));
117
+
118
+ // ACS fetch outcomes feed the host's connectivity banner (any response = up,
119
+ // thrown fetch = down). The host's sink stays host-side (lazy, no load cycle).
120
+ if (opts.onReachabilityChange) {
121
+ const onReach = opts.onReachabilityChange;
122
+ setAcsReachabilityReporter({ up: () => onReach(true), down: () => onReach(false) });
123
+ }
124
+
125
+ // L3 DeviceState (props.deviceState) — the SEMANTIC layer, wired with the SDK's
126
+ // OWN device-bridge collectors. The shell injects none (the R10 payoff): an
127
+ // undeclared/unimplemented domain returns explicit `unknown`s, never a silent
128
+ // undefined. Kept a SEPARATE statement from the SERVER relay — the two
129
+ // reasoning paths never merge.
130
+ setDeviceStateProvider(
131
+ createDeviceState({ connectivity: deviceConnectivity, location: deviceLocation }),
132
+ );
133
+
134
+ // LOCAL device tools — register the static, host-owned platform tools
135
+ // (device.echo / device.ping) at load, so the device path answers even before
136
+ // any plugin bundle or sign-in: the platform tier deliberately does not depend
137
+ // on the thing being proved. A tenant's EXTERNAL device tools load on
138
+ // session-authenticated, which the host wires to its auth-session subscription
139
+ // via syncDeviceToolsForSession() — ambient, not voice-triggered.
140
+ registerPlatformDeviceTools();
141
+
142
+ // Plugin/UI-action chips delegate navigation to the host's router.
143
+ setUIActionNavigator(opts.navigateToPlugin);
144
+
145
+ // Core config + token bridge. URL getters (not snapshots) so the env switch
146
+ // flows through; identity/token bridge owned by the host.
147
+ initAdas({
148
+ coreUrl: opts.urls.core,
149
+ ...(opts.urls.voice ? { voiceUrl: opts.urls.voice } : {}),
150
+ ...(opts.urls.consumerAuth ? { consumerAuthUrl: opts.urls.consumerAuth } : {}),
151
+ ...(opts.onSession ? { onSession: opts.onSession } : {}),
152
+ ...(opts.tenant ? { tenant: opts.tenant } : {}),
153
+ getUser: opts.getUser,
154
+ tokenProvider: opts.tokenProvider,
155
+ attachmentCache,
156
+ });
157
+
158
+ return { attachmentCache };
159
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The mounted half of the camera tasks — scanning and recording.
3
+ *
4
+ * The app renders this ONCE, near the root. It draws nothing until a plugin
5
+ * asks for a scan or a recording; then it takes over the screen with a live
6
+ * preview and settles the plugin's promise when the work is done.
7
+ *
8
+ * WHY IT IS VISIBLE, unlike VisualEngineHost's 1×1 invisible camera. These two
9
+ * tasks need the USER to aim: a barcode has to be framed, a clip has to point
10
+ * at something. An invisible scanner is not a privacy nicety, it is unusable —
11
+ * the user cannot tell what the camera sees or when it has read something.
12
+ *
13
+ * WHY IT CAN ALWAYS BE CANCELLED. The camera is on and pointed at whatever is
14
+ * in front of the person holding it. A capture surface with no way out is a
15
+ * trap; the cancel path settles the promise with a distinct CANCELLED code so
16
+ * the caller can tell "they chose not to" from "it broke".
17
+ */
18
+ import React, { useEffect, useRef, useState } from 'react';
19
+ import { Pressable, StyleSheet, Text, View } from 'react-native';
20
+ import { CameraView, useCameraPermissions, useMicrophonePermissions } from 'expo-camera';
21
+
22
+ import { cameraTasks, cameraError, type CameraTask } from './cameraTasks';
23
+
24
+ /** How long a scan waits before giving up. */
25
+ const SCAN_TIMEOUT_MS = 30_000;
26
+ /** Ceiling on a single clip, so a forgotten recording cannot fill the disk. */
27
+ const MAX_RECORD_MS = 60_000;
28
+
29
+ export function CameraTaskHost(): React.ReactElement | null {
30
+ const [task, setTask] = useState<CameraTask | null>(cameraTasks.current());
31
+ const [camPermission, requestCamPermission] = useCameraPermissions();
32
+ const [micPermission, requestMicPermission] = useMicrophonePermissions();
33
+ const cameraRef = useRef<CameraView | null>(null);
34
+ /** Guards against a barcode firing twice before the task is cleared. */
35
+ const settling = useRef(false);
36
+
37
+ // Announce that a surface exists. Without this the broker rejects every
38
+ // request immediately, which is the correct behaviour when no host is
39
+ // mounted — a plugin must never await a promise nothing can settle.
40
+ useEffect(() => {
41
+ cameraTasks.setHostMounted(true);
42
+ return () => cameraTasks.setHostMounted(false);
43
+ }, []);
44
+
45
+ useEffect(() => cameraTasks.subscribe(() => {
46
+ settling.current = false;
47
+ setTask(cameraTasks.current());
48
+ }), []);
49
+
50
+ // Ask for permission when a task arrives, not at app start: a camera prompt
51
+ // with no visible reason attached is one users refuse.
52
+ useEffect(() => {
53
+ if (!task) return;
54
+ let cancelled = false;
55
+ void (async () => {
56
+ const cam = camPermission?.granted ? camPermission : await requestCamPermission();
57
+ if (cancelled) return;
58
+ if (!cam?.granted) {
59
+ cameraTasks.fail(cameraError(
60
+ 'PERMISSION_DENIED',
61
+ 'Camera permission was not granted. Enable it for this app in Settings > Privacy > Camera.',
62
+ ));
63
+ return;
64
+ }
65
+ if (task.kind === 'record' && task.opts.withAudio) {
66
+ const mic = micPermission?.granted ? micPermission : await requestMicPermission();
67
+ if (cancelled) return;
68
+ if (!mic?.granted) {
69
+ // Refuse rather than silently recording a silent video — a caller
70
+ // that asked for audio and got none has no way to find out.
71
+ cameraTasks.fail(cameraError(
72
+ 'PERMISSION_DENIED',
73
+ 'Microphone permission was not granted, so a video with sound cannot be recorded.',
74
+ ));
75
+ }
76
+ }
77
+ })();
78
+ return () => { cancelled = true; };
79
+ }, [task]);
80
+
81
+ // Scan timeout. A scan that never finds a code must END, and say so: leaving
82
+ // the camera up indefinitely is both a battery drain and a promise that
83
+ // never settles.
84
+ useEffect(() => {
85
+ if (task?.kind !== 'scan') return;
86
+ const ms = task.opts.timeoutMs ?? SCAN_TIMEOUT_MS;
87
+ const t = setTimeout(() => {
88
+ cameraTasks.fail(cameraError('TIMEOUT', `No barcode was found within ${Math.round(ms / 1000)}s.`));
89
+ }, ms);
90
+ return () => clearTimeout(t);
91
+ }, [task]);
92
+
93
+ // Recording runs for its duration and resolves with the file.
94
+ useEffect(() => {
95
+ if (task?.kind !== 'record' || !camPermission?.granted) return;
96
+ let cancelled = false;
97
+ const maxMs = Math.min(task.opts.maxDurationMs ?? 15_000, MAX_RECORD_MS);
98
+ const startedAt = Date.now();
99
+
100
+ void (async () => {
101
+ // A frame late is better than a crash: the ref is attached on the render
102
+ // after the task arrives.
103
+ await new Promise(r => setTimeout(r, 50));
104
+ if (cancelled || !cameraRef.current) return;
105
+ try {
106
+ const video = await cameraRef.current.recordAsync({ maxDuration: Math.ceil(maxMs / 1000) });
107
+ if (cancelled) return;
108
+ if (!video?.uri) {
109
+ cameraTasks.fail(cameraError('INTERNAL', 'Recording finished but produced no file.'));
110
+ return;
111
+ }
112
+ cameraTasks.complete({ uri: video.uri, durationMs: Date.now() - startedAt });
113
+ } catch (err: any) {
114
+ if (!cancelled) cameraTasks.fail(cameraError('INTERNAL', err?.message || 'Recording failed.'));
115
+ }
116
+ })();
117
+
118
+ return () => {
119
+ cancelled = true;
120
+ // Stop the hardware if the surface goes away mid-take.
121
+ try { cameraRef.current?.stopRecording(); } catch { /* already stopped */ }
122
+ };
123
+ }, [task, camPermission?.granted]);
124
+
125
+ if (!task || !camPermission?.granted) return null;
126
+
127
+ const isScan = task.kind === 'scan';
128
+
129
+ return (
130
+ <View style={styles.overlay}>
131
+ <CameraView
132
+ ref={cameraRef}
133
+ style={StyleSheet.absoluteFill}
134
+ facing="back"
135
+ mode={isScan ? 'picture' : 'video'}
136
+ barcodeScannerSettings={
137
+ isScan && task.opts.formats?.length
138
+ ? { barcodeTypes: task.opts.formats as any }
139
+ : undefined
140
+ }
141
+ onBarcodeScanned={
142
+ isScan
143
+ ? ({ data, type }) => {
144
+ // onBarcodeScanned fires continuously while a code is in frame.
145
+ // Without this guard the same scan resolves, then tries to
146
+ // resolve an already-cleared task on the very next frame.
147
+ if (settling.current) return;
148
+ settling.current = true;
149
+ cameraTasks.complete({ data, format: type });
150
+ }
151
+ : undefined
152
+ }
153
+ />
154
+
155
+ <View style={styles.hud} pointerEvents="box-none">
156
+ <Text style={styles.hint}>
157
+ {isScan ? 'Point the camera at the code' : 'Recording…'}
158
+ </Text>
159
+ <Pressable style={styles.cancel} onPress={() => cameraTasks.cancel()} accessibilityRole="button">
160
+ <Text style={styles.cancelText}>Cancel</Text>
161
+ </Pressable>
162
+ </View>
163
+ </View>
164
+ );
165
+ }
166
+
167
+ const styles = StyleSheet.create({
168
+ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: '#000', zIndex: 9999 },
169
+ hud: { flex: 1, justifyContent: 'space-between', paddingVertical: 64, alignItems: 'center' },
170
+ hint: { color: '#fff', fontSize: 16, textAlign: 'center', paddingHorizontal: 24 },
171
+ cancel: { paddingHorizontal: 28, paddingVertical: 14, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.18)' },
172
+ cancelText: { color: '#fff', fontSize: 16, fontWeight: '600' },
173
+ });
174
+
175
+ export default CameraTaskHost;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The broker between a plugin's `await` and a mounted camera.
3
+ *
4
+ * WHY THIS EXISTS. expo-camera has no headless capture: scanning a barcode and
5
+ * recording a clip both require a MOUNTED `<CameraView>`. But a plugin calls
6
+ * `native.scanner.scanOnce()` and expects a promise — it has no way to mount
7
+ * anything, and it must not have to.
8
+ *
9
+ * So the API enqueues a TASK here and awaits it; `CameraTaskHost`, mounted once
10
+ * by the app, notices, shows the camera, does the work, and settles the
11
+ * promise. This is the same split as the Visual engine, for the same reason —
12
+ * and it is why `native.camera.scanBarcode` refuses and points here rather than
13
+ * pretending a function call can own a React lifecycle.
14
+ *
15
+ * ONE TASK AT A TIME, and it is the phone that says so: there is one camera. A
16
+ * second request while one is running is REJECTED rather than queued, because a
17
+ * queued scan would open the camera again minutes later, long after the user
18
+ * moved on and with no idea why it appeared.
19
+ */
20
+ export interface ScanTask {
21
+ kind: 'scan';
22
+ opts: { formats?: string[]; timeoutMs?: number };
23
+ resolve: (v: { data: string; format: string }) => void;
24
+ reject: (e: Error) => void;
25
+ }
26
+
27
+ export interface RecordTask {
28
+ kind: 'record';
29
+ opts: { maxDurationMs?: number; withAudio?: boolean };
30
+ resolve: (v: { uri: string; durationMs: number | null }) => void;
31
+ reject: (e: Error) => void;
32
+ }
33
+
34
+ export type CameraTask = ScanTask | RecordTask;
35
+
36
+ type Listener = () => void;
37
+
38
+ /** Typed failure, so a caller can say something specific and true. */
39
+ export function cameraError(code: string, message: string): Error {
40
+ const err: any = new Error(message);
41
+ err.code = code;
42
+ return err;
43
+ }
44
+
45
+ class CameraTaskBroker {
46
+ private task: CameraTask | null = null;
47
+ private listeners = new Set<Listener>();
48
+ /** Set by the host while a camera is mounted and ready. */
49
+ private hostMounted = false;
50
+
51
+ current(): CameraTask | null { return this.task; }
52
+
53
+ subscribe(fn: Listener): () => void {
54
+ this.listeners.add(fn);
55
+ return () => this.listeners.delete(fn);
56
+ }
57
+
58
+ /**
59
+ * Declare that a `CameraTaskHost` exists.
60
+ *
61
+ * Without it nothing will ever settle the promise, so a request must fail
62
+ * IMMEDIATELY and say why. The alternative is a plugin awaiting forever,
63
+ * which is the worst failure of the set: no error, no timeout, no camera,
64
+ * and a conversation that simply stops.
65
+ */
66
+ setHostMounted(mounted: boolean): void {
67
+ this.hostMounted = mounted;
68
+ if (!mounted) this.fail(cameraError('UNAVAILABLE', 'The camera surface was dismissed before the task completed.'));
69
+ this.notify();
70
+ }
71
+
72
+ isHostMounted(): boolean { return this.hostMounted; }
73
+
74
+ /** Enqueue and hand back the promise the plugin awaits. */
75
+ run<T>(kind: CameraTask['kind'], opts: any): Promise<T> {
76
+ if (!this.hostMounted) {
77
+ return Promise.reject(cameraError(
78
+ 'UNAVAILABLE',
79
+ 'No camera surface is mounted in this app, so the camera cannot be shown. ' +
80
+ 'The host must render <CameraTaskHost /> once, near the app root.',
81
+ ));
82
+ }
83
+ if (this.task) {
84
+ return Promise.reject(cameraError(
85
+ 'BUSY',
86
+ `The camera is already in use (${this.task.kind}). Only one camera task can run at a time.`,
87
+ ));
88
+ }
89
+ return new Promise<T>((resolve, reject) => {
90
+ this.task = { kind, opts, resolve, reject } as unknown as CameraTask;
91
+ this.notify();
92
+ });
93
+ }
94
+
95
+ /** Settle the running task successfully and clear it. */
96
+ complete(value: any): void {
97
+ const t = this.task;
98
+ if (!t) return;
99
+ this.task = null;
100
+ this.notify();
101
+ t.resolve(value);
102
+ }
103
+
104
+ /** Settle the running task with a failure and clear it. */
105
+ fail(err: Error): void {
106
+ const t = this.task;
107
+ if (!t) return;
108
+ this.task = null;
109
+ this.notify();
110
+ t.reject(err);
111
+ }
112
+
113
+ /**
114
+ * The user backed out.
115
+ *
116
+ * A distinct code, because "the user chose not to" is not a malfunction and
117
+ * a guide should say something different about it than about a broken lens.
118
+ */
119
+ cancel(): void {
120
+ this.fail(cameraError('CANCELLED', 'Cancelled — the camera was dismissed before anything was captured.'));
121
+ }
122
+
123
+ private notify(): void {
124
+ for (const fn of this.listeners) { try { fn(); } catch { /* a bad listener is not the broker's problem */ } }
125
+ }
126
+ }
127
+
128
+ /** One broker per app: there is one camera. */
129
+ export const cameraTasks = new CameraTaskBroker();