@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.
- package/android/build.gradle +16 -0
- package/android/src/main/java/expo/modules/adavision/AdaVisionModule.kt +75 -0
- package/dist/attachmentCache-DhQTqi2E.d.mts +18 -0
- package/dist/attachmentCache-DhQTqi2E.d.ts +18 -0
- package/dist/bootstrap.d.mts +95 -0
- package/dist/bootstrap.d.ts +95 -0
- package/dist/bootstrap.js +2473 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +64 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/chunk-4BMWVN25.mjs +91 -0
- package/dist/chunk-4BMWVN25.mjs.map +1 -0
- package/dist/chunk-4XYXJYN5.mjs +1513 -0
- package/dist/chunk-4XYXJYN5.mjs.map +1 -0
- package/dist/chunk-5HM6UA2K.mjs +571 -0
- package/dist/chunk-5HM6UA2K.mjs.map +1 -0
- package/dist/chunk-6LXF3JVX.mjs +96 -0
- package/dist/chunk-6LXF3JVX.mjs.map +1 -0
- package/dist/chunk-FF6ZZZXH.mjs +71 -0
- package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
- package/dist/chunk-KOXF2FZM.mjs +92 -0
- package/dist/chunk-KOXF2FZM.mjs.map +1 -0
- package/dist/chunk-L4V7OCJ3.mjs +71 -0
- package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
- package/dist/chunk-TZXWVYYR.mjs +67 -0
- package/dist/chunk-TZXWVYYR.mjs.map +1 -0
- package/dist/chunk-VQJPSLHP.mjs +1801 -0
- package/dist/chunk-VQJPSLHP.mjs.map +1 -0
- package/dist/chunk-WMJKH4XE.mjs +34 -0
- package/dist/chunk-WMJKH4XE.mjs.map +1 -0
- package/dist/chunk-XQSY6MHP.mjs +213 -0
- package/dist/chunk-XQSY6MHP.mjs.map +1 -0
- package/dist/createBridge.mjs +2 -0
- package/dist/createBridge.mjs.map +1 -1
- package/dist/device/createDeviceProvider.d.mts +23 -0
- package/dist/device/createDeviceProvider.d.ts +23 -0
- package/dist/device/createDeviceProvider.js +967 -0
- package/dist/device/createDeviceProvider.js.map +1 -0
- package/dist/device/createDeviceProvider.mjs +17 -0
- package/dist/device/createDeviceProvider.mjs.map +1 -0
- package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
- package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
- package/dist/device-bridge/collectors/appEngagement.js +128 -0
- package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
- package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
- package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
- package/dist/device-bridge/collectors/health.d.mts +24 -0
- package/dist/device-bridge/collectors/health.d.ts +24 -0
- package/dist/device-bridge/collectors/health.js +238 -0
- package/dist/device-bridge/collectors/health.js.map +1 -0
- package/dist/device-bridge/collectors/health.mjs +18 -0
- package/dist/device-bridge/collectors/health.mjs.map +1 -0
- package/dist/device-bridge/cookies.d.mts +49 -0
- package/dist/device-bridge/cookies.d.ts +49 -0
- package/dist/device-bridge/cookies.js +90 -0
- package/dist/device-bridge/cookies.js.map +1 -0
- package/dist/device-bridge/cookies.mjs +14 -0
- package/dist/device-bridge/cookies.mjs.map +1 -0
- package/dist/device-bridge/index.d.mts +483 -0
- package/dist/device-bridge/index.d.ts +483 -0
- package/dist/device-bridge/index.js +2084 -0
- package/dist/device-bridge/index.js.map +1 -0
- package/dist/device-bridge/index.mjs +32 -0
- package/dist/device-bridge/index.mjs.map +1 -0
- package/dist/device-bridge/nfc.d.mts +30 -0
- package/dist/device-bridge/nfc.d.ts +30 -0
- package/dist/device-bridge/nfc.js +109 -0
- package/dist/device-bridge/nfc.js.map +1 -0
- package/dist/device-bridge/nfc.mjs +14 -0
- package/dist/device-bridge/nfc.mjs.map +1 -0
- package/dist/index.d.mts +493 -18
- package/dist/index.d.ts +493 -18
- package/dist/index.js +1409 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -194
- package/dist/index.mjs.map +1 -1
- package/dist/oauth/expoOAuth.d.mts +18 -0
- package/dist/oauth/expoOAuth.d.ts +18 -0
- package/dist/oauth/expoOAuth.js +102 -0
- package/dist/oauth/expoOAuth.js.map +1 -0
- package/dist/oauth/expoOAuth.mjs +8 -0
- package/dist/oauth/expoOAuth.mjs.map +1 -0
- package/dist/types-BRanJYTn.d.mts +138 -0
- package/dist/types-BRanJYTn.d.ts +138 -0
- package/dist/vision/index.d.mts +81 -0
- package/dist/vision/index.d.ts +81 -0
- package/dist/vision/index.js +101 -0
- package/dist/vision/index.js.map +1 -0
- package/dist/vision/index.mjs +28 -0
- package/dist/vision/index.mjs.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AdaVision.podspec +28 -0
- package/ios/AdaVisionModule.swift +432 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
- package/package.json +107 -7
- package/src/bootstrap.ts +159 -0
- package/src/camera/CameraTaskHost.tsx +175 -0
- package/src/camera/cameraTasks.ts +129 -0
- package/src/device/createDeviceProvider.ts +832 -0
- package/src/device-bridge/actions/calendar-actions.ts +217 -0
- package/src/device-bridge/actions/dnd.ts +74 -0
- package/src/device-bridge/actions/health-actions.ts +58 -0
- package/src/device-bridge/actions/index.ts +12 -0
- package/src/device-bridge/actions/messaging.ts +74 -0
- package/src/device-bridge/actions/navigation.ts +88 -0
- package/src/device-bridge/actions/queue-poller.ts +151 -0
- package/src/device-bridge/background.ts +137 -0
- package/src/device-bridge/biometrics.ts +64 -0
- package/src/device-bridge/capabilities.ts +218 -0
- package/src/device-bridge/collectors/appEngagement.ts +120 -0
- package/src/device-bridge/collectors/battery.ts +83 -0
- package/src/device-bridge/collectors/calendar.ts +162 -0
- package/src/device-bridge/collectors/camera.ts +27 -0
- package/src/device-bridge/collectors/connectivity.ts +60 -0
- package/src/device-bridge/collectors/contacts.ts +200 -0
- package/src/device-bridge/collectors/health.ts +299 -0
- package/src/device-bridge/collectors/index.ts +12 -0
- package/src/device-bridge/collectors/location.ts +224 -0
- package/src/device-bridge/collectors/mood.ts +141 -0
- package/src/device-bridge/collectors/notifications.ts +70 -0
- package/src/device-bridge/cookies.ts +89 -0
- package/src/device-bridge/dynamic-connector.ts +422 -0
- package/src/device-bridge/index.ts +467 -0
- package/src/device-bridge/nfc.ts +85 -0
- package/src/device-bridge/plugin.ts +71 -0
- package/src/device-bridge/relay-client.ts +237 -0
- package/src/device-bridge/scanner.ts +42 -0
- package/src/device-bridge/sharing.ts +35 -0
- package/src/device-bridge/types.ts +326 -0
- package/src/device-state/activityClassifier.ts +151 -0
- package/src/device-state/activityEngine.ts +252 -0
- package/src/device-state/collectors.ts +31 -0
- package/src/device-state/domains/activity.ts +93 -0
- package/src/device-state/domains/capability.ts +60 -0
- package/src/device-state/domains/connectivity.ts +60 -0
- package/src/device-state/domains/deviceContext.ts +55 -0
- package/src/device-state/domains/interaction.ts +59 -0
- package/src/device-state/domains/spatial.ts +283 -0
- package/src/device-state/domains/visual.ts +100 -0
- package/src/device-state/headingSource.ts +84 -0
- package/src/device-state/index.ts +77 -0
- package/src/device-state/quality.ts +60 -0
- package/src/device-state/replay.ts +220 -0
- package/src/device-state/turnDetector.ts +121 -0
- package/src/device-state/visualEngine.ts +341 -0
- package/src/haptics.ts +4 -0
- package/src/index.ts +37 -0
- package/src/oauth/expoOAuth.tsx +113 -0
- package/src/vision/analysis.js +80 -0
- package/src/vision/index.ts +144 -0
- package/src/vision/ring.js +35 -0
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateam-ai-mobile/expo",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
13
|
-
"@ateam-ai-mobile/plugin-runtime": "
|
|
14
|
-
"@ateam-ai-mobile/ui": "
|
|
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
|
}
|
package/src/bootstrap.ts
ADDED
|
@@ -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();
|