@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.
- 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 +2991 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +66 -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-6LXF3JVX.mjs +96 -0
- package/dist/chunk-6LXF3JVX.mjs.map +1 -0
- package/dist/chunk-7FWYEHN5.mjs +203 -0
- package/dist/chunk-7FWYEHN5.mjs.map +1 -0
- package/dist/chunk-C7M5WAHA.mjs +1798 -0
- package/dist/chunk-C7M5WAHA.mjs.map +1 -0
- package/dist/chunk-FF6ZZZXH.mjs +71 -0
- package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
- package/dist/chunk-GF2MPTAB.mjs +647 -0
- package/dist/chunk-GF2MPTAB.mjs.map +1 -0
- package/dist/chunk-J6XRNX7P.mjs +19 -0
- package/dist/chunk-J6XRNX7P.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-PCAEUX2N.mjs +67 -0
- package/dist/chunk-PCAEUX2N.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/chunk-Z5AI2SXX.mjs +1810 -0
- package/dist/chunk-Z5AI2SXX.mjs.map +1 -0
- package/dist/createBridge.js +41 -10
- package/dist/createBridge.js.map +1 -1
- package/dist/createBridge.mjs +7 -1
- 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 +1228 -0
- package/dist/device/createDeviceProvider.js.map +1 -0
- package/dist/device/createDeviceProvider.mjs +19 -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 +495 -0
- package/dist/device-bridge/index.d.ts +495 -0
- package/dist/device-bridge/index.js +2121 -0
- package/dist/device-bridge/index.js.map +1 -0
- package/dist/device-bridge/index.mjs +34 -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 +509 -18
- package/dist/index.d.ts +509 -18
- package/dist/index.js +1706 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +24 -194
- package/dist/index.mjs.map +1 -1
- package/dist/maps/index.d.mts +56 -0
- package/dist/maps/index.d.ts +56 -0
- package/dist/maps/index.js +95 -0
- package/dist/maps/index.js.map +1 -0
- package/dist/maps/index.mjs +17 -0
- package/dist/maps/index.mjs.map +1 -0
- 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 +83 -0
- package/dist/vision/index.d.ts +83 -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/AdaMaps/AdaMapsModule.swift +180 -0
- package/ios/AdaMaps.podspec +25 -0
- package/ios/AdaVision.podspec +32 -0
- package/ios/AdaVisionModule.swift +486 -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 +113 -7
- package/src/args.ts +56 -0
- package/src/bootstrap.ts +159 -0
- package/src/camera/CameraTaskHost.tsx +175 -0
- package/src/camera/cameraTasks.ts +129 -0
- package/src/createBridge.ts +3 -1
- package/src/device/createDeviceProvider.ts +917 -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 +236 -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 +35 -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 +57 -0
- package/src/device-state/domains/interaction.ts +60 -0
- package/src/device-state/domains/spatial.ts +367 -0
- package/src/device-state/domains/visual.ts +121 -0
- package/src/device-state/events.ts +49 -0
- package/src/device-state/headingSource.ts +119 -0
- package/src/device-state/index.ts +78 -0
- package/src/device-state/quality.ts +60 -0
- package/src/device-state/replay.ts +285 -0
- package/src/device-state/spatialEvents.ts +300 -0
- package/src/device-state/turnDetector.ts +121 -0
- package/src/device-state/visualEngine.ts +341 -0
- package/src/geo.ts +23 -0
- package/src/haptics.ts +4 -0
- package/src/index.ts +37 -0
- package/src/maps/index.ts +81 -0
- package/src/maps/normalize.ts +225 -0
- package/src/oauth/expoOAuth.tsx +113 -0
- package/src/vision/analysis.js +80 -0
- package/src/vision/index.ts +146 -0
- package/src/vision/ring.js +35 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mobile-pa/device-bridge
|
|
3
|
+
*
|
|
4
|
+
* Isolated device data sync module. Zero coupling to chat, voice, or UI.
|
|
5
|
+
* Pluggable capability system — register built-in or custom capabilities.
|
|
6
|
+
*
|
|
7
|
+
* Usage (host app controls permission flow):
|
|
8
|
+
*
|
|
9
|
+
* import { DeviceBridge, capabilities } from '@mobile-pa/device-bridge';
|
|
10
|
+
*
|
|
11
|
+
* const bridge = new DeviceBridge({ relayUrl, deviceId, apiKey });
|
|
12
|
+
*
|
|
13
|
+
* // 1. Register capabilities
|
|
14
|
+
* bridge.registerAll(capabilities.allBuiltIn());
|
|
15
|
+
*
|
|
16
|
+
* // 2. Permission flow — host app shows its own UI per capability
|
|
17
|
+
* for (const cap of bridge.getPendingPermissions()) {
|
|
18
|
+
* // → show explanation: cap.name, cap.description, cap.reason
|
|
19
|
+
* // → user taps "Allow"
|
|
20
|
+
* await bridge.requestPermission(cap.id);
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* // 3. Start syncing (only syncs permitted capabilities)
|
|
24
|
+
* await bridge.startSync();
|
|
25
|
+
*
|
|
26
|
+
* // 4. Add more capabilities later (even after sync started)
|
|
27
|
+
* bridge.register(myCustomCapability);
|
|
28
|
+
* await bridge.requestPermission(myCustomCapability.id);
|
|
29
|
+
* // → auto-starts syncing if bridge is already running
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { RelayClient } from './relay-client';
|
|
33
|
+
import { QueuePoller } from './actions/queue-poller';
|
|
34
|
+
import { registerBackgroundSync, unregisterBackgroundSync } from './background';
|
|
35
|
+
import type {
|
|
36
|
+
DeviceBridgeConfig,
|
|
37
|
+
BridgeStatus,
|
|
38
|
+
CapabilityStatus,
|
|
39
|
+
DomainSyncStatus,
|
|
40
|
+
SyncState,
|
|
41
|
+
Capability,
|
|
42
|
+
PermissionState,
|
|
43
|
+
ActionResult,
|
|
44
|
+
} from './types';
|
|
45
|
+
|
|
46
|
+
// Re-export types
|
|
47
|
+
export type {
|
|
48
|
+
DeviceBridgeConfig,
|
|
49
|
+
BridgeStatus,
|
|
50
|
+
CapabilityStatus,
|
|
51
|
+
DomainSyncStatus,
|
|
52
|
+
BridgeError,
|
|
53
|
+
ActionResult,
|
|
54
|
+
Capability,
|
|
55
|
+
PermissionState,
|
|
56
|
+
} from './types';
|
|
57
|
+
|
|
58
|
+
// Re-export capability factories
|
|
59
|
+
export * as capabilities from './capabilities';
|
|
60
|
+
|
|
61
|
+
export class DeviceBridge {
|
|
62
|
+
private config: DeviceBridgeConfig;
|
|
63
|
+
private relay: RelayClient;
|
|
64
|
+
private queuePoller: QueuePoller;
|
|
65
|
+
|
|
66
|
+
// Capability registry
|
|
67
|
+
private caps: Map<string, Capability> = new Map();
|
|
68
|
+
private syncTimers: Map<string, ReturnType<typeof setInterval>> = new Map();
|
|
69
|
+
private subscriptions: Map<string, () => void> = new Map();
|
|
70
|
+
private domainStatus: Map<string, DomainSyncStatus> = new Map();
|
|
71
|
+
private running = false;
|
|
72
|
+
|
|
73
|
+
constructor(config: DeviceBridgeConfig) {
|
|
74
|
+
this.config = config;
|
|
75
|
+
|
|
76
|
+
this.relay = new RelayClient({
|
|
77
|
+
relayUrl: config.relayUrl,
|
|
78
|
+
deviceId: config.deviceId,
|
|
79
|
+
tenant: config.tenant,
|
|
80
|
+
actorId: config.actorId,
|
|
81
|
+
apiKey: config.apiKey,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
this.queuePoller = new QueuePoller({
|
|
85
|
+
relayClient: this.relay,
|
|
86
|
+
pollInterval: 5_000,
|
|
87
|
+
onActionExecuted: () => this.notifyStatusChange(),
|
|
88
|
+
onError: (error) => this.config.onError?.({
|
|
89
|
+
domain: 'actions',
|
|
90
|
+
message: error.message,
|
|
91
|
+
code: 'ACTION_ERROR',
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─── Capability Registration ───
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Register a capability. Does NOT request permission — just makes it known.
|
|
101
|
+
* Can be called before or after startSync().
|
|
102
|
+
*/
|
|
103
|
+
register(cap: Capability): void {
|
|
104
|
+
this.caps.set(cap.id, cap);
|
|
105
|
+
this.domainStatus.set(cap.id, {
|
|
106
|
+
domain: cap.id,
|
|
107
|
+
lastSync: null,
|
|
108
|
+
state: 'idle',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Register multiple capabilities at once.
|
|
114
|
+
*/
|
|
115
|
+
registerAll(caps: Capability[]): void {
|
|
116
|
+
for (const cap of caps) {
|
|
117
|
+
this.register(cap);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get a registered capability by ID.
|
|
123
|
+
* Use this to read name/description/reason for your permission UI.
|
|
124
|
+
*/
|
|
125
|
+
getCapability(id: string): Capability | undefined {
|
|
126
|
+
return this.caps.get(id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get all registered capabilities with their current status.
|
|
131
|
+
*/
|
|
132
|
+
listCapabilities(): CapabilityStatus[] {
|
|
133
|
+
return Array.from(this.caps.values()).map(cap => ({
|
|
134
|
+
id: cap.id,
|
|
135
|
+
name: cap.name,
|
|
136
|
+
permission: cap.permissionState,
|
|
137
|
+
sync: this.domainStatus.get(cap.id) || {
|
|
138
|
+
domain: cap.id,
|
|
139
|
+
lastSync: null,
|
|
140
|
+
state: 'idle' as SyncState,
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ─── Permissions (host app controls the flow) ───
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Request permission for ONE capability.
|
|
149
|
+
* Call after showing your own explanation screen.
|
|
150
|
+
* If bridge is already running and granted, syncing starts automatically.
|
|
151
|
+
*/
|
|
152
|
+
async requestPermission(capId: string): Promise<boolean> {
|
|
153
|
+
const cap = this.caps.get(capId);
|
|
154
|
+
if (!cap) throw new Error(`Capability "${capId}" not registered`);
|
|
155
|
+
|
|
156
|
+
if (!cap.requiresPermission) {
|
|
157
|
+
cap.permissionState = 'not_needed';
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const granted = await cap.requestPermission();
|
|
162
|
+
cap.permissionState = granted ? 'granted' : 'denied';
|
|
163
|
+
|
|
164
|
+
// Auto-start syncing if bridge is already running
|
|
165
|
+
if (granted && this.running) {
|
|
166
|
+
this.startCapabilitySync(cap);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.notifyStatusChange();
|
|
170
|
+
return granted;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check permission WITHOUT prompting.
|
|
175
|
+
*/
|
|
176
|
+
async checkPermission(capId: string): Promise<PermissionState> {
|
|
177
|
+
const cap = this.caps.get(capId);
|
|
178
|
+
if (!cap) throw new Error(`Capability "${capId}" not registered`);
|
|
179
|
+
if (!cap.requiresPermission) return 'not_needed';
|
|
180
|
+
|
|
181
|
+
const granted = await cap.checkPermission();
|
|
182
|
+
cap.permissionState = granted ? 'granted' : cap.permissionState;
|
|
183
|
+
return cap.permissionState;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Request ALL permissions sequentially (one at a time, not bombarded).
|
|
188
|
+
*/
|
|
189
|
+
async requestAllPermissions(): Promise<Map<string, boolean>> {
|
|
190
|
+
const results = new Map<string, boolean>();
|
|
191
|
+
|
|
192
|
+
for (const [id, cap] of this.caps) {
|
|
193
|
+
if (!cap.requiresPermission) {
|
|
194
|
+
cap.permissionState = 'not_needed';
|
|
195
|
+
results.set(id, true);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const granted = await cap.requestPermission();
|
|
199
|
+
cap.permissionState = granted ? 'granted' : 'denied';
|
|
200
|
+
results.set(id, granted);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.notifyStatusChange();
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get capabilities that still need permission.
|
|
209
|
+
* Use this to drive a "next permission" onboarding flow.
|
|
210
|
+
*/
|
|
211
|
+
getPendingPermissions(): Capability[] {
|
|
212
|
+
return Array.from(this.caps.values()).filter(
|
|
213
|
+
cap => cap.requiresPermission && cap.permissionState === 'not_requested'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Sync Control ───
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Start syncing all permitted capabilities + action queue poller.
|
|
221
|
+
*/
|
|
222
|
+
async startSync(): Promise<void> {
|
|
223
|
+
if (this.running) return;
|
|
224
|
+
this.running = true;
|
|
225
|
+
|
|
226
|
+
for (const [, cap] of this.caps) {
|
|
227
|
+
const permitted =
|
|
228
|
+
!cap.requiresPermission ||
|
|
229
|
+
cap.permissionState === 'granted' ||
|
|
230
|
+
cap.permissionState === 'not_needed';
|
|
231
|
+
|
|
232
|
+
if (permitted) {
|
|
233
|
+
this.startCapabilitySync(cap);
|
|
234
|
+
} else {
|
|
235
|
+
this.updateDomainStatus(cap.id, 'error', 'Permission not granted');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.queuePoller.start();
|
|
240
|
+
|
|
241
|
+
// R7: the bridge OWNS its background-fetch lifecycle. Registering the OS task
|
|
242
|
+
// here (from this bridge's own config) — instead of the host doing it
|
|
243
|
+
// separately — is what lets stopSync() reliably UNREGISTER it: before R7 the
|
|
244
|
+
// host registered it but stopBridge never unregistered, so the OS task kept
|
|
245
|
+
// firing with the PREVIOUS user's config across logout/tenant-switch.
|
|
246
|
+
// Best-effort: a host without expo-background-fetch just skips it. Needs a
|
|
247
|
+
// tenant + apiKey (both optional on DeviceBridgeConfig) to hit the relay —
|
|
248
|
+
// without them the background task has no identity to sync as, so skip it.
|
|
249
|
+
if (this.config.tenant && this.config.apiKey) {
|
|
250
|
+
registerBackgroundSync({
|
|
251
|
+
relayUrl: this.config.relayUrl,
|
|
252
|
+
deviceId: this.config.deviceId,
|
|
253
|
+
tenant: this.config.tenant,
|
|
254
|
+
actorId: this.config.actorId,
|
|
255
|
+
apiKey: this.config.apiKey,
|
|
256
|
+
}).catch((e: any) => console.warn('[adas/bridge] registerBackgroundSync failed:', e?.message || e));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.notifyStatusChange();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Stop all syncing.
|
|
264
|
+
*/
|
|
265
|
+
stopSync(): void {
|
|
266
|
+
this.running = false;
|
|
267
|
+
|
|
268
|
+
for (const [id, timer] of this.syncTimers) {
|
|
269
|
+
clearInterval(timer);
|
|
270
|
+
this.updateDomainStatus(id, 'stopped');
|
|
271
|
+
}
|
|
272
|
+
this.syncTimers.clear();
|
|
273
|
+
for (const unsub of this.subscriptions.values()) {
|
|
274
|
+
try { unsub(); } catch { /* swallow — best effort cleanup */ }
|
|
275
|
+
}
|
|
276
|
+
this.subscriptions.clear();
|
|
277
|
+
this.queuePoller.stop();
|
|
278
|
+
// R7: unregister the OS background-fetch task so it can't keep firing with
|
|
279
|
+
// this (now stale) user's config after logout/tenant-switch.
|
|
280
|
+
unregisterBackgroundSync().catch((e: any) => console.warn('[adas/bridge] unregisterBackgroundSync failed:', e?.message || e));
|
|
281
|
+
this.notifyStatusChange();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Force immediate sync of all permitted capabilities.
|
|
286
|
+
*/
|
|
287
|
+
async syncNow(): Promise<void> {
|
|
288
|
+
const promises = Array.from(this.caps.values())
|
|
289
|
+
.filter(c => c.permissionState === 'granted' || c.permissionState === 'not_needed')
|
|
290
|
+
.map(c => this.syncCapability(c));
|
|
291
|
+
await Promise.allSettled(promises);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Force immediate sync of a single capability by id. Used by the host to
|
|
296
|
+
* refresh fast-moving sources (battery) on app-foreground so the relay
|
|
297
|
+
* gets a fresh reading the moment the user opens the app.
|
|
298
|
+
*/
|
|
299
|
+
async syncCapabilityNow(capId: string): Promise<void> {
|
|
300
|
+
const cap = this.caps.get(capId);
|
|
301
|
+
if (!cap) return;
|
|
302
|
+
const permitted =
|
|
303
|
+
!cap.requiresPermission ||
|
|
304
|
+
cap.permissionState === 'granted' ||
|
|
305
|
+
cap.permissionState === 'not_needed';
|
|
306
|
+
if (!permitted) return;
|
|
307
|
+
await this.syncCapability(cap);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get current bridge status.
|
|
312
|
+
*/
|
|
313
|
+
getStatus(): BridgeStatus {
|
|
314
|
+
return {
|
|
315
|
+
running: this.running,
|
|
316
|
+
capabilities: this.listCapabilities(),
|
|
317
|
+
lastFullSync: this.getLastFullSync(),
|
|
318
|
+
pendingActions: this.relay.getOfflineQueueSize(),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if the relay is reachable.
|
|
324
|
+
*/
|
|
325
|
+
async checkConnectivity(): Promise<boolean> {
|
|
326
|
+
return this.relay.ping();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Internal ───
|
|
330
|
+
|
|
331
|
+
private startCapabilitySync(cap: Capability): void {
|
|
332
|
+
if (this.syncTimers.has(cap.id)) return;
|
|
333
|
+
|
|
334
|
+
const interval = this.config.syncIntervals?.[cap.id] ?? cap.defaultInterval;
|
|
335
|
+
this.syncCapability(cap);
|
|
336
|
+
const timer = setInterval(() => this.syncCapability(cap), interval);
|
|
337
|
+
this.syncTimers.set(cap.id, timer);
|
|
338
|
+
|
|
339
|
+
// Wire any OS-event subscription (e.g. battery level/state) so we push
|
|
340
|
+
// on real change rather than just on the polling cadence. iOS suspends
|
|
341
|
+
// JS timers in the background, which is exactly when these events still
|
|
342
|
+
// fire from native.
|
|
343
|
+
if (cap.subscribe && !this.subscriptions.has(cap.id)) {
|
|
344
|
+
try {
|
|
345
|
+
const unsub = cap.subscribe(() => {
|
|
346
|
+
if (this.running) this.syncCapability(cap);
|
|
347
|
+
});
|
|
348
|
+
this.subscriptions.set(cap.id, unsub);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
351
|
+
this.config.onError?.({
|
|
352
|
+
domain: cap.id,
|
|
353
|
+
message: `subscribe failed: ${message}`,
|
|
354
|
+
code: 'SUBSCRIBE_ERROR',
|
|
355
|
+
timestamp: new Date().toISOString(),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async syncCapability(cap: Capability): Promise<void> {
|
|
362
|
+
if (!this.running) return;
|
|
363
|
+
this.updateDomainStatus(cap.id, 'syncing');
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const data = await cap.collect();
|
|
367
|
+
// Collectors that did the work and concluded "nothing to push" return
|
|
368
|
+
// null (e.g. contacts hashing detects no changes since last cycle —
|
|
369
|
+
// saves a 1MB+ payload for 6000 contacts). Stay 'idle', don't hit the
|
|
370
|
+
// network, but don't treat it as an error either.
|
|
371
|
+
if (data === null) {
|
|
372
|
+
this.updateDomainStatus(cap.id, 'idle');
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const result = await this.relay.pushDomain(cap.id, data);
|
|
376
|
+
|
|
377
|
+
if (result.ok) {
|
|
378
|
+
this.updateDomainStatus(cap.id, 'idle');
|
|
379
|
+
} else {
|
|
380
|
+
this.updateDomainStatus(cap.id, 'error', result.error);
|
|
381
|
+
this.config.onError?.({
|
|
382
|
+
domain: cap.id,
|
|
383
|
+
message: result.error || 'Push failed',
|
|
384
|
+
code: 'RELAY_ERROR',
|
|
385
|
+
timestamp: new Date().toISOString(),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
} catch (error) {
|
|
389
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
+
this.updateDomainStatus(cap.id, 'error', message);
|
|
391
|
+
this.config.onError?.({
|
|
392
|
+
domain: cap.id,
|
|
393
|
+
message,
|
|
394
|
+
code: 'COLLECTOR_ERROR',
|
|
395
|
+
timestamp: new Date().toISOString(),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private updateDomainStatus(id: string, state: SyncState, error?: string): void {
|
|
401
|
+
const current = this.domainStatus.get(id);
|
|
402
|
+
this.domainStatus.set(id, {
|
|
403
|
+
domain: id,
|
|
404
|
+
lastSync: state === 'idle' ? new Date().toISOString() : (current?.lastSync ?? null),
|
|
405
|
+
state,
|
|
406
|
+
error,
|
|
407
|
+
itemCount: current?.itemCount,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private getLastFullSync(): string | null {
|
|
412
|
+
const syncs = Array.from(this.domainStatus.values())
|
|
413
|
+
.map(d => d.lastSync)
|
|
414
|
+
.filter((s): s is string => s !== null);
|
|
415
|
+
if (syncs.length === 0) return null;
|
|
416
|
+
return syncs.sort()[0];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private notifyStatusChange(): void {
|
|
420
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ─── Background Sync Support ───
|
|
425
|
+
export {
|
|
426
|
+
registerBackgroundSync,
|
|
427
|
+
unregisterBackgroundSync,
|
|
428
|
+
syncDomains,
|
|
429
|
+
BACKGROUND_TASK_NAME,
|
|
430
|
+
} from './background';
|
|
431
|
+
export type { BackgroundSyncConfig } from './background';
|
|
432
|
+
|
|
433
|
+
// ─── Re-exports for advanced usage ───
|
|
434
|
+
export { RelayClient } from './relay-client';
|
|
435
|
+
export { QueuePoller } from './actions/queue-poller';
|
|
436
|
+
|
|
437
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
438
|
+
// PLATFORM NATIVE CAPABILITIES
|
|
439
|
+
// device-bridge owns every native dependency. plugin-sdk delegates to
|
|
440
|
+
// these namespaces so RN plugins never import native modules directly.
|
|
441
|
+
// Adding a new native capability = one file here + one plugin-sdk
|
|
442
|
+
// delegation + one rebuild — THEN every future solution consumes it
|
|
443
|
+
// as plain JS, no rebuild required.
|
|
444
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
// Biometrics — Face ID / Touch ID / fingerprint (expo-local-authentication)
|
|
447
|
+
export * as biometrics from './biometrics';
|
|
448
|
+
export type { BiometricType, BiometricCapabilities, AuthenticateOptions } from './biometrics';
|
|
449
|
+
|
|
450
|
+
// Sharing — native share sheet (expo-sharing)
|
|
451
|
+
export * as sharing from './sharing';
|
|
452
|
+
export type { ShareOptions } from './sharing';
|
|
453
|
+
|
|
454
|
+
// Scanner — QR / barcode camera (expo-camera)
|
|
455
|
+
export * as scanner from './scanner';
|
|
456
|
+
export type { ScanResult } from './scanner';
|
|
457
|
+
|
|
458
|
+
// Location — GPS fixes for plugin tools (expo-location, safe in Expo Go)
|
|
459
|
+
export * as location from './collectors/location';
|
|
460
|
+
export type { Position } from './collectors/location';
|
|
461
|
+
|
|
462
|
+
// NOTE: cookies (@react-native-cookies/cookies) and nfc (react-native-nfc-manager)
|
|
463
|
+
// require native modules NOT included in Expo Go. They are imported lazily
|
|
464
|
+
// via require() in useNativeCapabilities.ts. Do NOT add eager exports here
|
|
465
|
+
// or the app will crash in Expo Go.
|
|
466
|
+
// To use: require('../../packages/device-bridge/src/cookies')
|
|
467
|
+
// To use: require('../../packages/device-bridge/src/nfc')
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-bridge: NFC
|
|
3
|
+
*
|
|
4
|
+
* Wraps react-native-nfc-manager for:
|
|
5
|
+
* - Reading NFC tags (NDEF, Mifare)
|
|
6
|
+
* - Writing to writable tags
|
|
7
|
+
* - App-triggered tag discovery
|
|
8
|
+
*
|
|
9
|
+
* Use cases: physical-world triggers, inventory lookup, hardware keys,
|
|
10
|
+
* loyalty cards, transit top-up, pairing.
|
|
11
|
+
*
|
|
12
|
+
* iOS: requires "Near Field Communication Tag Reading" entitlement in the
|
|
13
|
+
* app's provisioning profile. Android: requires NFC permission in manifest.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import NfcManager, { NfcTech, Ndef } from 'react-native-nfc-manager';
|
|
17
|
+
|
|
18
|
+
let _initialized = false;
|
|
19
|
+
|
|
20
|
+
async function ensureInit(): Promise<boolean> {
|
|
21
|
+
if (_initialized) return true;
|
|
22
|
+
try {
|
|
23
|
+
const supported = await NfcManager.isSupported();
|
|
24
|
+
if (!supported) return false;
|
|
25
|
+
await NfcManager.start();
|
|
26
|
+
_initialized = true;
|
|
27
|
+
return true;
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function isSupported(): Promise<boolean> {
|
|
34
|
+
try { return await NfcManager.isSupported(); } catch { return false; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function isEnabled(): Promise<boolean> {
|
|
38
|
+
try { return await NfcManager.isEnabled(); } catch { return false; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NdefRecord {
|
|
42
|
+
type: string;
|
|
43
|
+
payload: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TagReadResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
tagId?: string;
|
|
49
|
+
records?: NdefRecord[];
|
|
50
|
+
error?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Wait for the next tag to be scanned, read NDEF records, release, and return. */
|
|
54
|
+
export async function readOnce(timeoutMs: number = 30_000): Promise<TagReadResult> {
|
|
55
|
+
const ready = await ensureInit();
|
|
56
|
+
if (!ready) return { ok: false, error: 'nfc_unsupported' };
|
|
57
|
+
try {
|
|
58
|
+
const tag = await Promise.race([
|
|
59
|
+
NfcManager.requestTechnology(NfcTech.Ndef).then(() => NfcManager.getTag()),
|
|
60
|
+
new Promise<null>((resolve) => setTimeout(() => resolve(null), timeoutMs)),
|
|
61
|
+
]);
|
|
62
|
+
if (!tag) {
|
|
63
|
+
try { await NfcManager.cancelTechnologyRequest(); } catch {}
|
|
64
|
+
return { ok: false, error: 'timeout' };
|
|
65
|
+
}
|
|
66
|
+
const records: NdefRecord[] = [];
|
|
67
|
+
if ((tag as any).ndefMessage) {
|
|
68
|
+
for (const r of (tag as any).ndefMessage) {
|
|
69
|
+
try {
|
|
70
|
+
const type = String.fromCharCode(...(r.type || []));
|
|
71
|
+
const payload = Ndef.text.decodePayload(r.payload || []);
|
|
72
|
+
records.push({ type, payload: String(payload) });
|
|
73
|
+
} catch {
|
|
74
|
+
// skip records we can't decode
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const tagId = (tag as any).id ? Array.from((tag as any).id).map((b: any) => b.toString(16).padStart(2, '0')).join('') : undefined;
|
|
79
|
+
return { ok: true, tagId, records };
|
|
80
|
+
} catch (e: any) {
|
|
81
|
+
return { ok: false, error: String(e?.message || e) };
|
|
82
|
+
} finally {
|
|
83
|
+
try { await NfcManager.cancelTechnologyRequest(); } catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service plugin wrapper for ateam-mobile.
|
|
3
|
+
*
|
|
4
|
+
* Wraps DeviceBridge as a RegisteredServicePlugin so the dynamic
|
|
5
|
+
* connector loader can start/stop it as part of the solution lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: In the current architecture, device-bridge is managed directly
|
|
8
|
+
* by capabilitiesStore (which owns the DeviceBridge instance and controls
|
|
9
|
+
* which capabilities are registered based on user preferences).
|
|
10
|
+
*
|
|
11
|
+
* This plugin wrapper is kept for potential future use where solutions
|
|
12
|
+
* might declare device-bridge as a functional connector with custom config.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { DeviceBridge, capabilities } from './index';
|
|
16
|
+
|
|
17
|
+
export interface DeviceBridgePluginConfig {
|
|
18
|
+
relayUrl: string;
|
|
19
|
+
deviceId: string;
|
|
20
|
+
tenant: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
/** Override sync intervals per domain (ms). */
|
|
23
|
+
syncIntervals?: Record<string, number>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Create a service plugin definition for ateam-mobile's plugin registry.
|
|
28
|
+
* Returns the shape expected by PluginSDK.registerService().
|
|
29
|
+
*/
|
|
30
|
+
export function createDeviceBridgePlugin(config: DeviceBridgePluginConfig) {
|
|
31
|
+
let bridge: DeviceBridge | null = null;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
type: 'service' as const,
|
|
35
|
+
capabilities: {
|
|
36
|
+
location: true,
|
|
37
|
+
notifications: true,
|
|
38
|
+
},
|
|
39
|
+
hooks: {
|
|
40
|
+
onAppStart: true,
|
|
41
|
+
onForeground: true,
|
|
42
|
+
onBackground: true,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async onStart() {
|
|
46
|
+
if (bridge) return; // already running
|
|
47
|
+
|
|
48
|
+
bridge = new DeviceBridge({
|
|
49
|
+
relayUrl: config.relayUrl,
|
|
50
|
+
deviceId: config.deviceId,
|
|
51
|
+
tenant: config.tenant,
|
|
52
|
+
apiKey: config.apiKey,
|
|
53
|
+
syncIntervals: config.syncIntervals,
|
|
54
|
+
onError: (err) => {
|
|
55
|
+
// Silence transient network errors — relay-client already buffers offline.
|
|
56
|
+
if (/Network error|Network request failed|Offline/i.test(err.message)) return;
|
|
57
|
+
console.warn(`[device-bridge] ${err.domain}: ${err.message}`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
bridge.registerAll(capabilities.allBuiltIn());
|
|
62
|
+
await bridge.requestAllPermissions();
|
|
63
|
+
await bridge.startSync();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async onStop() {
|
|
67
|
+
bridge?.stopSync();
|
|
68
|
+
bridge = null;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|