@ateam-ai-mobile/expo 0.1.5 → 0.1.8

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 (179) 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 +2991 -0
  8. package/dist/bootstrap.js.map +1 -0
  9. package/dist/bootstrap.mjs +66 -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-6LXF3JVX.mjs +96 -0
  14. package/dist/chunk-6LXF3JVX.mjs.map +1 -0
  15. package/dist/chunk-7FWYEHN5.mjs +203 -0
  16. package/dist/chunk-7FWYEHN5.mjs.map +1 -0
  17. package/dist/chunk-C7M5WAHA.mjs +1798 -0
  18. package/dist/chunk-C7M5WAHA.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-GF2MPTAB.mjs +647 -0
  22. package/dist/chunk-GF2MPTAB.mjs.map +1 -0
  23. package/dist/chunk-J6XRNX7P.mjs +19 -0
  24. package/dist/chunk-J6XRNX7P.mjs.map +1 -0
  25. package/dist/chunk-KOXF2FZM.mjs +92 -0
  26. package/dist/chunk-KOXF2FZM.mjs.map +1 -0
  27. package/dist/chunk-L4V7OCJ3.mjs +71 -0
  28. package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
  29. package/dist/chunk-PCAEUX2N.mjs +67 -0
  30. package/dist/chunk-PCAEUX2N.mjs.map +1 -0
  31. package/dist/chunk-WMJKH4XE.mjs +34 -0
  32. package/dist/chunk-WMJKH4XE.mjs.map +1 -0
  33. package/dist/chunk-XQSY6MHP.mjs +213 -0
  34. package/dist/chunk-XQSY6MHP.mjs.map +1 -0
  35. package/dist/chunk-Z5AI2SXX.mjs +1810 -0
  36. package/dist/chunk-Z5AI2SXX.mjs.map +1 -0
  37. package/dist/createBridge.js +41 -10
  38. package/dist/createBridge.js.map +1 -1
  39. package/dist/createBridge.mjs +7 -1
  40. package/dist/createBridge.mjs.map +1 -1
  41. package/dist/device/createDeviceProvider.d.mts +23 -0
  42. package/dist/device/createDeviceProvider.d.ts +23 -0
  43. package/dist/device/createDeviceProvider.js +1228 -0
  44. package/dist/device/createDeviceProvider.js.map +1 -0
  45. package/dist/device/createDeviceProvider.mjs +19 -0
  46. package/dist/device/createDeviceProvider.mjs.map +1 -0
  47. package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
  48. package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
  49. package/dist/device-bridge/collectors/appEngagement.js +128 -0
  50. package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
  51. package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
  52. package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
  53. package/dist/device-bridge/collectors/health.d.mts +24 -0
  54. package/dist/device-bridge/collectors/health.d.ts +24 -0
  55. package/dist/device-bridge/collectors/health.js +238 -0
  56. package/dist/device-bridge/collectors/health.js.map +1 -0
  57. package/dist/device-bridge/collectors/health.mjs +18 -0
  58. package/dist/device-bridge/collectors/health.mjs.map +1 -0
  59. package/dist/device-bridge/cookies.d.mts +49 -0
  60. package/dist/device-bridge/cookies.d.ts +49 -0
  61. package/dist/device-bridge/cookies.js +90 -0
  62. package/dist/device-bridge/cookies.js.map +1 -0
  63. package/dist/device-bridge/cookies.mjs +14 -0
  64. package/dist/device-bridge/cookies.mjs.map +1 -0
  65. package/dist/device-bridge/index.d.mts +495 -0
  66. package/dist/device-bridge/index.d.ts +495 -0
  67. package/dist/device-bridge/index.js +2121 -0
  68. package/dist/device-bridge/index.js.map +1 -0
  69. package/dist/device-bridge/index.mjs +34 -0
  70. package/dist/device-bridge/index.mjs.map +1 -0
  71. package/dist/device-bridge/nfc.d.mts +30 -0
  72. package/dist/device-bridge/nfc.d.ts +30 -0
  73. package/dist/device-bridge/nfc.js +109 -0
  74. package/dist/device-bridge/nfc.js.map +1 -0
  75. package/dist/device-bridge/nfc.mjs +14 -0
  76. package/dist/device-bridge/nfc.mjs.map +1 -0
  77. package/dist/index.d.mts +509 -18
  78. package/dist/index.d.ts +509 -18
  79. package/dist/index.js +1706 -6
  80. package/dist/index.js.map +1 -1
  81. package/dist/index.mjs +24 -194
  82. package/dist/index.mjs.map +1 -1
  83. package/dist/maps/index.d.mts +56 -0
  84. package/dist/maps/index.d.ts +56 -0
  85. package/dist/maps/index.js +95 -0
  86. package/dist/maps/index.js.map +1 -0
  87. package/dist/maps/index.mjs +17 -0
  88. package/dist/maps/index.mjs.map +1 -0
  89. package/dist/oauth/expoOAuth.d.mts +18 -0
  90. package/dist/oauth/expoOAuth.d.ts +18 -0
  91. package/dist/oauth/expoOAuth.js +102 -0
  92. package/dist/oauth/expoOAuth.js.map +1 -0
  93. package/dist/oauth/expoOAuth.mjs +8 -0
  94. package/dist/oauth/expoOAuth.mjs.map +1 -0
  95. package/dist/types-BRanJYTn.d.mts +138 -0
  96. package/dist/types-BRanJYTn.d.ts +138 -0
  97. package/dist/vision/index.d.mts +83 -0
  98. package/dist/vision/index.d.ts +83 -0
  99. package/dist/vision/index.js +101 -0
  100. package/dist/vision/index.js.map +1 -0
  101. package/dist/vision/index.mjs +28 -0
  102. package/dist/vision/index.mjs.map +1 -0
  103. package/expo-module.config.json +9 -0
  104. package/ios/AdaMaps/AdaMapsModule.swift +180 -0
  105. package/ios/AdaMaps.podspec +25 -0
  106. package/ios/AdaVision.podspec +32 -0
  107. package/ios/AdaVisionModule.swift +486 -0
  108. package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
  109. package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
  110. package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
  111. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
  112. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
  113. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
  114. package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
  115. package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
  116. package/package.json +113 -7
  117. package/src/args.ts +56 -0
  118. package/src/bootstrap.ts +159 -0
  119. package/src/camera/CameraTaskHost.tsx +175 -0
  120. package/src/camera/cameraTasks.ts +129 -0
  121. package/src/createBridge.ts +3 -1
  122. package/src/device/createDeviceProvider.ts +917 -0
  123. package/src/device-bridge/actions/calendar-actions.ts +217 -0
  124. package/src/device-bridge/actions/dnd.ts +74 -0
  125. package/src/device-bridge/actions/health-actions.ts +58 -0
  126. package/src/device-bridge/actions/index.ts +12 -0
  127. package/src/device-bridge/actions/messaging.ts +74 -0
  128. package/src/device-bridge/actions/navigation.ts +88 -0
  129. package/src/device-bridge/actions/queue-poller.ts +151 -0
  130. package/src/device-bridge/background.ts +137 -0
  131. package/src/device-bridge/biometrics.ts +64 -0
  132. package/src/device-bridge/capabilities.ts +218 -0
  133. package/src/device-bridge/collectors/appEngagement.ts +120 -0
  134. package/src/device-bridge/collectors/battery.ts +83 -0
  135. package/src/device-bridge/collectors/calendar.ts +162 -0
  136. package/src/device-bridge/collectors/camera.ts +27 -0
  137. package/src/device-bridge/collectors/connectivity.ts +60 -0
  138. package/src/device-bridge/collectors/contacts.ts +200 -0
  139. package/src/device-bridge/collectors/health.ts +299 -0
  140. package/src/device-bridge/collectors/index.ts +12 -0
  141. package/src/device-bridge/collectors/location.ts +236 -0
  142. package/src/device-bridge/collectors/mood.ts +141 -0
  143. package/src/device-bridge/collectors/notifications.ts +70 -0
  144. package/src/device-bridge/cookies.ts +89 -0
  145. package/src/device-bridge/dynamic-connector.ts +422 -0
  146. package/src/device-bridge/index.ts +467 -0
  147. package/src/device-bridge/nfc.ts +85 -0
  148. package/src/device-bridge/plugin.ts +71 -0
  149. package/src/device-bridge/relay-client.ts +237 -0
  150. package/src/device-bridge/scanner.ts +42 -0
  151. package/src/device-bridge/sharing.ts +35 -0
  152. package/src/device-bridge/types.ts +326 -0
  153. package/src/device-state/activityClassifier.ts +151 -0
  154. package/src/device-state/activityEngine.ts +252 -0
  155. package/src/device-state/collectors.ts +35 -0
  156. package/src/device-state/domains/activity.ts +93 -0
  157. package/src/device-state/domains/capability.ts +60 -0
  158. package/src/device-state/domains/connectivity.ts +60 -0
  159. package/src/device-state/domains/deviceContext.ts +57 -0
  160. package/src/device-state/domains/interaction.ts +60 -0
  161. package/src/device-state/domains/spatial.ts +367 -0
  162. package/src/device-state/domains/visual.ts +121 -0
  163. package/src/device-state/events.ts +49 -0
  164. package/src/device-state/headingSource.ts +119 -0
  165. package/src/device-state/index.ts +78 -0
  166. package/src/device-state/quality.ts +60 -0
  167. package/src/device-state/replay.ts +285 -0
  168. package/src/device-state/spatialEvents.ts +300 -0
  169. package/src/device-state/turnDetector.ts +121 -0
  170. package/src/device-state/visualEngine.ts +341 -0
  171. package/src/geo.ts +23 -0
  172. package/src/haptics.ts +4 -0
  173. package/src/index.ts +37 -0
  174. package/src/maps/index.ts +81 -0
  175. package/src/maps/normalize.ts +225 -0
  176. package/src/oauth/expoOAuth.tsx +113 -0
  177. package/src/vision/analysis.js +80 -0
  178. package/src/vision/index.ts +146 -0
  179. package/src/vision/ring.js +35 -0
@@ -0,0 +1,225 @@
1
+ // Pure shaping for native.maps — no Expo, no React Native, so it runs under
2
+ // plain `node --test` (tests/adaMapsNormalize.test.mjs pins it).
3
+ //
4
+ // The native side hands back loose dictionaries — AdaMaps (MapKit) for search
5
+ // and route, expo-location's geocoder for reverse geocoding. This turns them
6
+ // into the typed MapsAPI shapes, and one set of failure codes. Unknown is
7
+ // `null`, never '' or 0: an empty street reads as a street with no name, and a
8
+ // 0 m route reads as "you have arrived".
9
+ //
10
+ // It also checks what a caller sends BEFORE it crosses the bridge (searchArgs,
11
+ // coordinateArg). A remote bundle is plain JS at runtime, so the types promise
12
+ // nothing: a NaN or Infinity `limit` traps Swift's Int conversion and kills the
13
+ // app, and a `{ lat, lng }` near would search around 0,0 and answer "found
14
+ // nothing" with confidence.
15
+
16
+ import type {
17
+ MapsAddress,
18
+ MapsCoordinate,
19
+ MapsPlace,
20
+ MapsRoute,
21
+ MapsRouteStep,
22
+ } from '@ateam-ai-mobile/plugin-runtime';
23
+ import { distanceOf } from '../geo';
24
+ import { countArg, refuseArgument as refuse, shown, type ArgumentError } from '../args';
25
+
26
+ /** An address as the native geocoders spell it. AdaMaps sends exactly these
27
+ * keys; expo-location sends these plus `subregion`/`timezone`/
28
+ * `formattedAddress`, which MapsAddress does not carry. */
29
+ export interface RawMapsAddress {
30
+ name?: string | null;
31
+ street?: string | null;
32
+ streetNumber?: string | null;
33
+ district?: string | null;
34
+ city?: string | null;
35
+ region?: string | null;
36
+ postalCode?: string | null;
37
+ country?: string | null;
38
+ isoCountryCode?: string | null;
39
+ }
40
+
41
+ /** One AdaMaps search hit (AdaMapsModule.place). */
42
+ export interface RawMapsPlace {
43
+ name: string | null;
44
+ latitude: number;
45
+ longitude: number;
46
+ /** MKPointOfInterestCategory raw value, e.g. "MKPOICategoryPharmacy". */
47
+ category: string | null;
48
+ address: RawMapsAddress;
49
+ }
50
+
51
+ /** An AdaMaps walking route (AdaMapsModule.route) — distances in metres,
52
+ * expectedTravelTime in SECONDS, as MapKit reports them. */
53
+ export interface RawMapsRoute {
54
+ distance: number;
55
+ expectedTravelTime: number;
56
+ steps: Array<{ instructions: string | null; distance: number; notice: string | null; polyline: MapsCoordinate[] }>;
57
+ polyline: MapsCoordinate[];
58
+ }
59
+
60
+ /** The three ways a maps call can fail to look — see MapsAPI. */
61
+ export type MapsFailureCode = 'UNAVAILABLE' | 'OFFLINE' | 'THROTTLED';
62
+
63
+ /** A malformed call, refused before anything is sent. Not a fourth way to fail
64
+ * to look: retrying will not help, fixing the call will. */
65
+ export type MapsArgumentError = ArgumentError;
66
+
67
+ const SEARCH_RADIUS_DEFAULT_M = 3000;
68
+ /** Keeps the region MapKit is handed a small, valid one. A "near" search wider
69
+ * than this is a different question. */
70
+ const SEARCH_RADIUS_MAX_M = 100_000;
71
+ const SEARCH_LIMIT_DEFAULT = 8;
72
+ const SEARCH_LIMIT_MAX = 25;
73
+
74
+ const text = (v: unknown): string | null =>
75
+ typeof v === 'string' && v.trim() !== '' ? v : null;
76
+
77
+ const refuseArgument = (verb: string, problem: string, nothing: string): MapsArgumentError =>
78
+ refuse('maps', verb, problem, nothing);
79
+
80
+ /** A caller's coordinate, checked and copied to exactly `{ latitude, longitude }`.
81
+ * Anything else — `{ lat, lng }`, strings, NaN, out of range — refuses BY NAME
82
+ * rather than being sent as 0,0 or NaN. */
83
+ export function coordinateArg(verb: string, name: string, value: unknown, nothing: string): MapsCoordinate {
84
+ const c = value as { latitude?: unknown; longitude?: unknown } | null | undefined;
85
+ const latitude = c?.latitude, longitude = c?.longitude;
86
+ if (
87
+ typeof latitude === 'number' && Number.isFinite(latitude) && Math.abs(latitude) <= 90 &&
88
+ typeof longitude === 'number' && Number.isFinite(longitude) && Math.abs(longitude) <= 180
89
+ ) {
90
+ return { latitude, longitude };
91
+ }
92
+ throw refuseArgument(
93
+ verb,
94
+ `${name} must be { latitude, longitude } numbers (latitude -90..90, longitude -180..180), not ${shown(value)}`,
95
+ nothing,
96
+ );
97
+ }
98
+
99
+ /** search's arguments, checked and defaulted (radiusM 3000, limit 8). `limit`
100
+ * is floored and capped at 25; everything else out of range refuses. */
101
+ export function searchArgs(
102
+ query: unknown,
103
+ opts: { near?: unknown; radiusM?: unknown; limit?: unknown } | null | undefined,
104
+ ): { query: string; near: MapsCoordinate | null; radiusM: number; limit: number } {
105
+ const nothing = 'nothing was searched';
106
+ if (typeof query !== 'string' || query.trim() === '') {
107
+ throw refuseArgument('search', `query must be a non-empty string, not ${shown(query)}`, nothing);
108
+ }
109
+ const near = opts?.near == null ? null : coordinateArg('search', 'near', opts.near, nothing);
110
+ const radiusM = opts?.radiusM ?? SEARCH_RADIUS_DEFAULT_M;
111
+ if (typeof radiusM !== 'number' || !(radiusM > 0 && radiusM <= SEARCH_RADIUS_MAX_M)) {
112
+ throw refuseArgument(
113
+ 'search', `radiusM must be metres above 0 and at most ${SEARCH_RADIUS_MAX_M}, not ${shown(radiusM)}`, nothing);
114
+ }
115
+ const limit = countArg('maps', 'search', 'limit', opts?.limit, SEARCH_LIMIT_DEFAULT, SEARCH_LIMIT_MAX, nothing);
116
+ return { query, near, radiusM, limit };
117
+ }
118
+
119
+ const point = (c: MapsCoordinate): MapsCoordinate => ({
120
+ latitude: Number(c.latitude),
121
+ longitude: Number(c.longitude),
122
+ });
123
+
124
+ export function toMapsAddress(raw: RawMapsAddress | null | undefined): MapsAddress {
125
+ const r = raw ?? {};
126
+ return {
127
+ name: text(r.name),
128
+ street: text(r.street),
129
+ streetNumber: text(r.streetNumber),
130
+ district: text(r.district),
131
+ city: text(r.city),
132
+ region: text(r.region),
133
+ postalCode: text(r.postalCode),
134
+ country: text(r.country),
135
+ isoCountryCode: text(r.isoCountryCode),
136
+ };
137
+ }
138
+
139
+ /** "MKPOICategoryPharmacy" → "Pharmacy": the vocabulary term, without the
140
+ * framework's prefix. */
141
+ export function toMapsCategory(raw: unknown): string | null {
142
+ const c = text(raw);
143
+ return c ? c.replace(/^MKPOICategory/, '') || null : null;
144
+ }
145
+
146
+ /** Search hits → places. `distanceM` is straight-line from `near`, and only
147
+ * when the caller gave one — a distance from nowhere would be invented. */
148
+ export function toMapsPlaces(raw: RawMapsPlace[] | null | undefined, near: MapsCoordinate | null): MapsPlace[] {
149
+ return (raw ?? []).map(r => {
150
+ const coordinate = point(r);
151
+ return {
152
+ name: text(r.name),
153
+ coordinate,
154
+ address: toMapsAddress(r.address),
155
+ category: toMapsCategory(r.category),
156
+ distanceM: near ? distanceOf([near, coordinate]) : null,
157
+ };
158
+ });
159
+ }
160
+
161
+ /** A reverse-geocoded address → a place AT the point asked about. The platform
162
+ * geocoder returns an address, not a snapped point or a category. */
163
+ export function toReverseGeocodedPlace(at: MapsCoordinate, address: MapsAddress): MapsPlace {
164
+ return {
165
+ name: address.name,
166
+ coordinate: point(at),
167
+ address,
168
+ category: null,
169
+ distanceM: null,
170
+ };
171
+ }
172
+
173
+ /** A MapKit route → MapsRoute. Seconds become ms; every step is kept, including
174
+ * Apple's empty-instruction origin step, so indexes match Apple's. */
175
+ export function toMapsRoute(raw: RawMapsRoute): MapsRoute {
176
+ const steps: MapsRouteStep[] = (raw.steps ?? []).map(s => ({
177
+ instructions: typeof s.instructions === 'string' ? s.instructions : '',
178
+ distanceM: Number(s.distance),
179
+ notice: text(s.notice),
180
+ polyline: (s.polyline ?? []).map(point),
181
+ }));
182
+ return {
183
+ mode: 'walking',
184
+ distanceM: Number(raw.distance),
185
+ expectedTravelTimeMs: Math.round(Number(raw.expectedTravelTime) * 1000),
186
+ steps,
187
+ polyline: (raw.polyline ?? []).map(point),
188
+ };
189
+ }
190
+
191
+ /**
192
+ * A native rejection → an Error whose `code` is one of MapsAPI's three.
193
+ *
194
+ * AdaMaps already rejects with OFFLINE / THROTTLED / UNAVAILABLE. expo-location
195
+ * (reverseGeocode) has its own codes: on iOS a CLError.network becomes
196
+ * ERR_GEOCODING_NETWORK, which Apple uses for BOTH a lost connection and a rate
197
+ * limit — so it maps to OFFLINE and the message says it may be either. Anything
198
+ * else is UNAVAILABLE, with the platform's own words kept beside ours.
199
+ */
200
+ export function mapsFailure(verb: string, err: unknown): Error & { code: MapsFailureCode } {
201
+ const nativeCode = String((err as any)?.code ?? '');
202
+ const detail = String((err as any)?.message ?? err ?? '').trim();
203
+ let code: MapsFailureCode;
204
+ let why: string;
205
+ if (nativeCode === 'THROTTLED') {
206
+ code = 'THROTTLED';
207
+ why = 'Apple is rate-limiting map requests from this app — wait before retrying';
208
+ } else if (nativeCode === 'OFFLINE') {
209
+ code = 'OFFLINE';
210
+ why = 'the map service could not be reached — the phone may be offline';
211
+ } else if (nativeCode === 'ERR_GEOCODING_NETWORK') {
212
+ code = 'OFFLINE';
213
+ why = 'the geocoder could not be reached — the phone may be offline, or Apple is ' +
214
+ 'rate-limiting requests (iOS reports both the same way)';
215
+ } else {
216
+ code = 'UNAVAILABLE';
217
+ why = 'the map service failed';
218
+ }
219
+ const failure = new Error(
220
+ `maps.${verb} could not look: ${why}. This is not "found nothing".` +
221
+ (detail ? ` (${nativeCode ? nativeCode + ': ' : ''}${detail})` : ''),
222
+ ) as Error & { code: MapsFailureCode };
223
+ failure.code = code;
224
+ return failure;
225
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @ateam-ai-mobile/expo/oauth — the native OAuth provider for the relocated
3
+ * auth screens (LoginScreen / JoinScreen / OpenJoinScreen in @ateam-ai-mobile/ui).
4
+ *
5
+ * This is the ONLY place @react-native-google-signin + expo-apple-authentication
6
+ * are imported. The `ui` package stays native-free: its screens call
7
+ * getOAuthProvider() for the id_token acquisition + the real Apple HIG button.
8
+ *
9
+ * Deliberately a SUBPATH (`@ateam-ai-mobile/expo/oauth`), not part of the main
10
+ * entry — same rule as `/bridge` and `/device`: a host that doesn't install
11
+ * these native modules must still be able to build @ateam-ai-mobile/expo.
12
+ *
13
+ * Behaviour is ported verbatim from the app's old login.tsx so the iPhone
14
+ * parity gate sees no change:
15
+ * - Google: configure({ webClientId, iosClientId }) → hasPlayServices →
16
+ * signIn → normalize the v15-vs-older id_token shape. A missing iosClientId
17
+ * silently falls back to a web flow (unchanged); the caller gates on
18
+ * webClientId, so we never reach here without one for the login path.
19
+ * - a user-cancel (SIGN_IN_CANCELLED / IN_PROGRESS / ERR_REQUEST_CANCELED)
20
+ * resolves to null — the screen treats null as "no error", never a red flash.
21
+ */
22
+ import React from 'react';
23
+ import { Platform } from 'react-native';
24
+ import {
25
+ GoogleSignin,
26
+ statusCodes,
27
+ type ConfigureParams,
28
+ } from '@react-native-google-signin/google-signin';
29
+ import * as AppleAuthentication from 'expo-apple-authentication';
30
+ import type { OAuthProvider, OAuthGoogleConfig } from '@ateam-ai-mobile/ui';
31
+
32
+ const NOOP = () => {};
33
+
34
+ export interface ExpoOAuthConfig {
35
+ /**
36
+ * The host's bundled Google client ids, used by the join / open-join screens
37
+ * that carry no tenant config (they pass no cfg to signInWithGoogle). The
38
+ * login screen passes the tenant's configured ids and overrides these.
39
+ */
40
+ googleWebClientId?: string;
41
+ googleIosClientId?: string;
42
+ }
43
+
44
+ /**
45
+ * Build the native OAuth provider. Wire it once at startup:
46
+ * setOAuthProvider(createExpoOAuthProvider({ googleWebClientId, googleIosClientId }));
47
+ */
48
+ export function createExpoOAuthProvider(host: ExpoOAuthConfig = {}): OAuthProvider {
49
+ return {
50
+ async signInWithGoogle(cfg?: OAuthGoogleConfig): Promise<string | null> {
51
+ const webClientId = cfg?.webClientId || host.googleWebClientId || '';
52
+ const iosClientId = cfg?.iosClientId || host.googleIosClientId || '';
53
+ // configure is idempotent + cheap; do it at sign-in time so the tenant's
54
+ // ids are always current (replaces login.tsx's configure effect).
55
+ const params: ConfigureParams = { webClientId };
56
+ if (iosClientId) (params as any).iosClientId = iosClientId;
57
+ GoogleSignin.configure(params);
58
+ try {
59
+ // android-only; no-op on iOS. Called defensively, as before.
60
+ await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
61
+ const result: any = await GoogleSignin.signIn();
62
+ // v15+ returns { data: { idToken } }; older returns { idToken }.
63
+ return result?.data?.idToken || result?.idToken || null;
64
+ } catch (err: any) {
65
+ const code = err?.code;
66
+ if (code === statusCodes.SIGN_IN_CANCELLED || code === statusCodes.IN_PROGRESS) {
67
+ return null;
68
+ }
69
+ throw err;
70
+ }
71
+ },
72
+
73
+ async signInWithApple(): Promise<string | null> {
74
+ try {
75
+ const credential = await AppleAuthentication.signInAsync({
76
+ requestedScopes: [
77
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
78
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
79
+ ],
80
+ });
81
+ return credential.identityToken || null;
82
+ } catch (err: any) {
83
+ // ERR_REQUEST_CANCELED = the user dismissed the sheet.
84
+ if (err?.code === 'ERR_REQUEST_CANCELED') return null;
85
+ throw err;
86
+ }
87
+ },
88
+
89
+ async isAppleAvailable(): Promise<boolean> {
90
+ if (Platform.OS !== 'ios') return false;
91
+ try {
92
+ return await AppleAuthentication.isAvailableAsync();
93
+ } catch {
94
+ return false;
95
+ }
96
+ },
97
+
98
+ renderAppleButton({ onPress, disabled, style }): React.ReactNode {
99
+ // Apple's HIG requires their own styled button when Apple Sign-In sits
100
+ // alongside other providers. Same chrome the app shipped: CONTINUE /
101
+ // BLACK / fully-rounded. Layout (width/height) comes from the screen.
102
+ return (
103
+ <AppleAuthentication.AppleAuthenticationButton
104
+ buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
105
+ buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
106
+ cornerRadius={999}
107
+ style={style as any}
108
+ onPress={disabled ? NOOP : onPress}
109
+ />
110
+ );
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,80 @@
1
+ // Frame-quality analysis — the REFERENCE algorithm, in plain JS so it can be
2
+ // unit-tested exhaustively without a device. The native iOS (Objective-C++) and
3
+ // Android (Kotlin) implementations mirror this math and these thresholds exactly;
4
+ // this file is the spec they must match, and the thing the tests exercise.
5
+ //
6
+ // Input is a luma (grayscale) plane — one byte per pixel, 0..255. We subsample
7
+ // to a fixed grid so the work is bounded and thresholds are resolution-independent,
8
+ // then derive:
9
+ // exposure from mean luminance (good | dark | bright)
10
+ // obstructed from darkness + low variance (covered lens vs a real scene)
11
+ // blur from variance of the Laplacian (the classic focus metric)
12
+
13
+ export const THRESHOLDS = {
14
+ grid: 128,
15
+ exposureDark: 45,
16
+ exposureBright: 205,
17
+ obstructedMean: 22,
18
+ obstructedStd: 8,
19
+ blurSharp: 120,
20
+ blurSlight: 30,
21
+ };
22
+
23
+ /**
24
+ * @param {number} width
25
+ * @param {number} height
26
+ * @param {Uint8Array|number[]} luma row-major luma bytes
27
+ * @param {number} [rowStride] bytes per row (defaults to width)
28
+ * @returns {{blur:string, exposure:string, obstructed:boolean|null}}
29
+ */
30
+ export function assessLuma(width, height, luma, rowStride) {
31
+ const stride = rowStride || width;
32
+ const S = THRESHOLDS.grid;
33
+ if (!width || !height || !luma || luma.length === 0) {
34
+ return { blur: 'unknown', exposure: 'unknown', obstructed: null };
35
+ }
36
+
37
+ // Grid dimensions, capped at S. Sample coordinates span the FULL frame
38
+ // [0, width-1] / [0, height-1] — the first sample is the top-left pixel and
39
+ // the last is the bottom-right, so no edge (a bright window, an occlusion,
40
+ // strong detail) is ever omitted. A floor-step + capped-count scheme would
41
+ // systematically miss the right/bottom strip when the size isn't a multiple
42
+ // of S.
43
+ const cols = Math.min(S, width);
44
+ const rows = Math.min(S, height);
45
+ const sxAt = (gx) => (cols > 1 ? Math.round((gx * (width - 1)) / (cols - 1)) : 0);
46
+ const syAt = (gy) => (rows > 1 ? Math.round((gy * (height - 1)) / (rows - 1)) : 0);
47
+
48
+ const grid = new Uint8Array(cols * rows);
49
+ let n = 0, sum = 0, sumSq = 0;
50
+ for (let gy = 0; gy < rows; gy++) {
51
+ const rowBase = syAt(gy) * stride;
52
+ for (let gx = 0; gx < cols; gx++) {
53
+ const v = luma[rowBase + sxAt(gx)] & 0xff;
54
+ grid[gy * cols + gx] = v;
55
+ sum += v; sumSq += v * v; n++;
56
+ }
57
+ }
58
+ if (n === 0) return { blur: 'unknown', exposure: 'unknown', obstructed: null };
59
+
60
+ const mean = sum / n;
61
+ const variance = Math.max(0, sumSq / n - mean * mean);
62
+ const std = Math.sqrt(variance);
63
+
64
+ let lapSum = 0, lapSumSq = 0, lapN = 0;
65
+ for (let y = 1; y < rows - 1; y++) {
66
+ for (let x = 1; x < cols - 1; x++) {
67
+ const i = y * cols + x;
68
+ const lap = 4 * grid[i] - grid[i - 1] - grid[i + 1] - grid[i - cols] - grid[i + cols];
69
+ lapSum += lap; lapSumSq += lap * lap; lapN++;
70
+ }
71
+ }
72
+ const lapVar = lapN > 0 ? Math.max(0, lapSumSq / lapN - (lapSum / lapN) ** 2) : 0;
73
+
74
+ const t = THRESHOLDS;
75
+ const exposure = mean < t.exposureDark ? 'dark' : (mean > t.exposureBright ? 'bright' : 'good');
76
+ const obstructed = mean < t.obstructedMean && std < t.obstructedStd;
77
+ const blur = lapVar > t.blurSharp ? 'sharp' : (lapVar > t.blurSlight ? 'slight' : 'severe');
78
+
79
+ return { blur, exposure, obstructed };
80
+ }
@@ -0,0 +1,146 @@
1
+ // ada-vision — on-device image understanding, our own native code (no camera lib).
2
+ //
3
+ // On-device, frames never leave the phone:
4
+ // assessQuality(uri) — one-shot quality of a LOCAL frame file.
5
+ // startVision()/onVisionFrame — the headless camera session that publishes a
6
+ // timeline frame (id + quality, never pixels) at a
7
+ // cadence; readText/describe/detectObjects resolve
8
+ // a published id ON-DEVICE.
9
+ //
10
+ // The luma math mirrors ./analysis.js (which has the exhaustive unit tests).
11
+ //
12
+ // requireOptionalNativeModule: on a binary WITHOUT the native module, the import
13
+ // is null, isSupported() is false, and the session calls refuse cleanly (no crash).
14
+
15
+ import type { ComponentType } from 'react';
16
+ import { requireOptionalNativeModule } from 'expo-modules-core';
17
+ import { requireNativeView } from 'expo';
18
+
19
+ /** TEST-ONLY live preview bound to the same headless session (so a tester can
20
+ * aim). The real Guide runs headless and never mounts this. Null on a binary
21
+ * without the native view, so importing this module never crashes. */
22
+ export const CameraPreview: ComponentType<any> | null = (() => {
23
+ try { return requireNativeView('AdaVision'); } catch { return null; }
24
+ })();
25
+
26
+ export interface RawFrameQuality {
27
+ blur: 'sharp' | 'slight' | 'severe' | 'unknown';
28
+ exposure: 'good' | 'dark' | 'bright' | 'unknown';
29
+ /** true = lens covered; false = clearly not; null = not assessed. */
30
+ obstructed: boolean | null;
31
+ }
32
+
33
+ interface Subscription { remove(): void }
34
+
35
+ /** A published timeline frame from the vision session (metadata only — never
36
+ * pixels; the buffer stays in the native ring, resolved by id). */
37
+ export interface VisionFrame {
38
+ id: string;
39
+ ts: number;
40
+ expiresAt: number;
41
+ blur: RawFrameQuality['blur'];
42
+ exposure: RawFrameQuality['exposure'];
43
+ obstructed: RawFrameQuality['obstructed'];
44
+ }
45
+
46
+ /** One OCR result on a frame — matches the SDK's VisionText. */
47
+ export interface VisionText {
48
+ text: string;
49
+ /** 0..1 recogniser confidence (never a 1.0 filler). */
50
+ confidence: number | null;
51
+ /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native
52
+ * rotates frames at capture) — x runs from the user's left to their right. */
53
+ box?: { x: number; y: number; width: number; height: number };
54
+ }
55
+
56
+ /** One classifier label — a vocabulary term, NOT prose. Matches VisionLabel. */
57
+ export interface VisionLabel {
58
+ label: string;
59
+ confidence: number | null;
60
+ }
61
+
62
+ /** One detected object — a located item with a box, from the bundled detector. */
63
+ export interface VisionObject {
64
+ /** Open-Images class name (e.g. "Car", "Traffic light", "Building", "Person"). */
65
+ label: string;
66
+ /** 0..1 detector confidence. */
67
+ confidence: number | null;
68
+ /** Normalised 0..1 box, origin TOP-left, in the UPRIGHT portrait frame (native
69
+ * rotates frames at capture) — x runs from the user's left to their right. */
70
+ box?: { x: number; y: number; width: number; height: number };
71
+ }
72
+
73
+ interface AdaVisionNative {
74
+ isSupported(): boolean;
75
+ assessQuality(uri: string): Promise<RawFrameQuality>;
76
+ startVision(intervalMs: number): Promise<void>;
77
+ stopVision(): void;
78
+ visionRetentionMs(): number;
79
+ qualityById(id: string): RawFrameQuality | null;
80
+ readText(id: string, languages: string[] | null): Promise<VisionText[]>;
81
+ describe(id: string, maxLabels: number): Promise<VisionLabel[]>;
82
+ detectObjects(id: string, maxItems: number, minConfidence: number): Promise<VisionObject[]>;
83
+ addListener(event: 'visionFrame', cb: (f: VisionFrame) => void): Subscription;
84
+ }
85
+
86
+ const native = requireOptionalNativeModule<AdaVisionNative>('AdaVision');
87
+
88
+ /** Is the native vision module present in this binary? */
89
+ export function isSupported(): boolean {
90
+ try { return native?.isSupported() ?? false; } catch { return false; }
91
+ }
92
+
93
+ /** One-shot: measure a frame's quality from its LOCAL file. Throws if it cannot look. */
94
+ export async function assessQuality(uri: string): Promise<RawFrameQuality> {
95
+ if (!native) throw new Error('ada-vision native module not present in this build');
96
+ return native.assessQuality(uri);
97
+ }
98
+
99
+ // ── Vision session (feeds the Visual Engine) ──
100
+
101
+ /** Start the vision session — publishes a timeline frame every `intervalMs`. */
102
+ export async function startVision(intervalMs = 1000): Promise<void> {
103
+ if (!native) throw new Error('ada-vision native module not present in this build');
104
+ return native.startVision(intervalMs);
105
+ }
106
+
107
+ /** Stop the vision session and release the camera. */
108
+ export function stopVision(): void {
109
+ native?.stopVision?.();
110
+ }
111
+
112
+ /** Subscribe to published timeline frames (id + quality + expiry; no pixels). */
113
+ export function onVisionFrame(cb: (f: VisionFrame) => void): Subscription {
114
+ if (!native) return { remove() {} };
115
+ return native.addListener('visionFrame', cb);
116
+ }
117
+
118
+ /** How far back the native ring can still resolve a frame id (ms). */
119
+ export function visionRetentionMs(): number {
120
+ return native?.visionRetentionMs?.() ?? 0;
121
+ }
122
+
123
+ /** Resolve a published frame id → its measured quality (null if evicted). */
124
+ export function qualityById(id: string): RawFrameQuality | null {
125
+ return native?.qualityById?.(id) ?? null;
126
+ }
127
+
128
+ /** On-device OCR on a published frame. [] = looked, no text; throws = couldn't look. */
129
+ export async function readText(id: string, languages?: string[]): Promise<VisionText[]> {
130
+ if (!native) throw new Error('ada-vision native module not present in this build');
131
+ return native.readText(id, languages ?? null);
132
+ }
133
+
134
+ /** On-device image classification → labels (not prose). [] = nothing recognised. */
135
+ export async function describe(id: string, maxLabels = 5): Promise<VisionLabel[]> {
136
+ if (!native) throw new Error('ada-vision native module not present in this build');
137
+ return native.describe(id, maxLabels);
138
+ }
139
+
140
+ /** On-device object detection (bundled YOLOv8n, Open-Images vocabulary) → located
141
+ * items with boxes. [] = looked, found nothing above `minConfidence`; throws =
142
+ * couldn't look (id aged out, or the model isn't in this build). */
143
+ export async function detectObjects(id: string, maxItems = 10, minConfidence = 0.25): Promise<VisionObject[]> {
144
+ if (!native) throw new Error('ada-vision native module not present in this build');
145
+ return native.detectObjects(id, maxItems, minConfidence);
146
+ }
@@ -0,0 +1,35 @@
1
+ // Reference for the native VisionSession frame ring (ios/AdaVisionModule.swift).
2
+ // The native side mirrors this; the tests pin the invariant GPT flagged:
3
+ // a frame id from an OLD camera session, or one evicted from the ring, must
4
+ // NEVER resolve. "Cannot look at that frame" (null) must never be confusable
5
+ // with a stale hit from a previous session.
6
+ //
7
+ // Two independent guarantees, both modelled here: ids are generation-scoped
8
+ // (vf_<gen>_<ts>), AND the ring is cleared on start(). Either alone prevents a
9
+ // stale resolve; together it can't happen.
10
+
11
+ export function createRing({ cap = 12, intervalMs = 1000 } = {}) {
12
+ let generation = 0;
13
+ let entries = []; // { id, quality }
14
+
15
+ return {
16
+ /** A new camera session: bump generation, clear the ring. */
17
+ start() { generation += 1; entries = []; },
18
+ /** Session ended: nothing is resolvable. */
19
+ stop() { entries = []; },
20
+ /** The ring's actual retention (ms) — feeds the engine's window clamp. */
21
+ retentionMs() { return cap * intervalMs; },
22
+ /** Publish a frame → mint a generation-scoped id, retain (bounded). */
23
+ publish(quality, tsMs) {
24
+ const id = `vf_${generation}_${tsMs}`;
25
+ entries.push({ id, quality });
26
+ if (entries.length > cap) entries.splice(0, entries.length - cap);
27
+ return id;
28
+ },
29
+ /** Resolve by id → quality, or null when evicted / old-generation / unknown. */
30
+ qualityById(id) {
31
+ const e = entries.find(x => x.id === id);
32
+ return e ? e.quality : null;
33
+ },
34
+ };
35
+ }