@ateam-ai-mobile/expo 0.1.0

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.
@@ -0,0 +1,609 @@
1
+ // src/createBridge.ts
2
+ import AsyncStorage from "@react-native-async-storage/async-storage";
3
+ import * as ExpoCalendar from "expo-calendar";
4
+ import * as ExpoContacts from "expo-contacts";
5
+ import * as ExpoLocation from "expo-location";
6
+ import * as ExpoBattery from "expo-battery";
7
+ import * as ExpoNetwork from "expo-network";
8
+ import * as ExpoNotifications from "expo-notifications";
9
+ import * as ExpoSMS from "expo-sms";
10
+ import * as ExpoLinking from "expo-linking";
11
+ import { Platform } from "react-native";
12
+ function createBridge(connectorId, deviceId) {
13
+ const storagePrefix = `connector:${connectorId}:`;
14
+ const log = {
15
+ info: (msg, data) => {
16
+ console.log(`[${connectorId}] ${msg}`, data || "");
17
+ },
18
+ warn: (msg, data) => {
19
+ console.warn(`[${connectorId}] ${msg}`, data || "");
20
+ },
21
+ error: (msg, data) => {
22
+ console.error(`[${connectorId}] ${msg}`, data || "");
23
+ }
24
+ };
25
+ const parseDateTime = (dateStr, timeStr) => {
26
+ const [year, month, day] = dateStr.split("-").map(Number);
27
+ const [hours, minutes] = timeStr.split(":").map(Number);
28
+ return new Date(year, month - 1, day, hours, minutes);
29
+ };
30
+ const formatDate = (date) => {
31
+ return date.toISOString().split("T")[0];
32
+ };
33
+ const getDayOfWeek = (date) => {
34
+ const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
35
+ return days[date.getDay()];
36
+ };
37
+ const addDays = (date, days) => {
38
+ const result = new Date(date);
39
+ result.setDate(result.getDate() + days);
40
+ return result;
41
+ };
42
+ const startOfDay = (date) => {
43
+ const result = new Date(date);
44
+ result.setHours(0, 0, 0, 0);
45
+ return result;
46
+ };
47
+ const endOfDay = (date) => {
48
+ const result = new Date(date);
49
+ result.setHours(23, 59, 59, 999);
50
+ return result;
51
+ };
52
+ const getCalendarIds = async () => {
53
+ try {
54
+ const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);
55
+ return calendars.map((c) => c.id);
56
+ } catch (err) {
57
+ log.warn("Failed to get calendar IDs", err);
58
+ return [];
59
+ }
60
+ };
61
+ const getDefaultCalendarId = async () => {
62
+ try {
63
+ const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);
64
+ return calendars.length > 0 ? calendars[0].id : "";
65
+ } catch (err) {
66
+ log.warn("Failed to get default calendar", err);
67
+ return "";
68
+ }
69
+ };
70
+ const formatCalendarEvent = (event) => {
71
+ return {
72
+ id: event.id,
73
+ title: event.title,
74
+ date: event.startDate ? formatDate(new Date(event.startDate)) : "",
75
+ start: event.startDate ? new Date(event.startDate).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) : "",
76
+ end: event.endDate ? new Date(event.endDate).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }) : "",
77
+ location: event.location || "",
78
+ attendees: event.attendees ? event.attendees.map((a) => a.email || a.name) : [],
79
+ notes: event.notes || ""
80
+ };
81
+ };
82
+ const formatContact = (contact) => {
83
+ const phone = contact.phoneNumbers?.[0]?.number || null;
84
+ const email = contact.emails?.[0]?.email || null;
85
+ const birthday = contact.birthday ? `${contact.birthday.year}-${String(contact.birthday.month).padStart(2, "0")}-${String(contact.birthday.day).padStart(2, "0")}` : null;
86
+ return {
87
+ id: contact.id,
88
+ name: contact.name,
89
+ phone,
90
+ email,
91
+ relationship: contact.relationships?.[0]?.relationship || contact.contactType || "contact",
92
+ birthday,
93
+ notes: contact.note || ""
94
+ };
95
+ };
96
+ const classifyLocation = (geo) => {
97
+ const name = geo.name?.toLowerCase() || "";
98
+ if (name.includes("home")) return "home";
99
+ if (name.includes("work") || name.includes("office")) return "work";
100
+ if (name.includes("school") || name.includes("university")) return "school";
101
+ if (name.includes("gym") || name.includes("fitness")) return "fitness";
102
+ if (name.includes("hospital") || name.includes("clinic") || name.includes("medical")) return "medical";
103
+ return "other";
104
+ };
105
+ const formatPlace = (geo) => {
106
+ if (!geo) return "Unknown Location";
107
+ const name = geo.name || "";
108
+ const street = geo.street ? `${geo.street}` : "";
109
+ return name ? `${name} \u2014 ${street}` : street;
110
+ };
111
+ const formatAddress = (geo) => {
112
+ if (!geo) return "";
113
+ const parts = [geo.street, geo.city, geo.region, geo.postalCode].filter(Boolean);
114
+ return parts.join(", ");
115
+ };
116
+ const formatNotification = (notif) => {
117
+ return {
118
+ id: notif.request?.identifier || String(Math.random()),
119
+ app: notif.request?.content?.title || "Unknown",
120
+ title: notif.request?.content?.title || "",
121
+ body: notif.request?.content?.body || "",
122
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
123
+ sender: null,
124
+ priority: "medium",
125
+ read: false
126
+ };
127
+ };
128
+ const mapBatteryState = (state) => {
129
+ if (state === ExpoBattery.BatteryState.CHARGING) return "charging";
130
+ if (state === ExpoBattery.BatteryState.FULL) return "full";
131
+ return "unplugged";
132
+ };
133
+ const bridge = {
134
+ calendar: {
135
+ async getToday() {
136
+ try {
137
+ const start = startOfDay(/* @__PURE__ */ new Date());
138
+ const end = endOfDay(/* @__PURE__ */ new Date());
139
+ const calendarIds = await getCalendarIds();
140
+ const events = await ExpoCalendar.getEventsAsync(calendarIds, start, end);
141
+ return {
142
+ date: formatDate(/* @__PURE__ */ new Date()),
143
+ day_of_week: getDayOfWeek(/* @__PURE__ */ new Date()),
144
+ events: events.map(formatCalendarEvent),
145
+ count: events.length
146
+ };
147
+ } catch (err) {
148
+ log.error("Failed to get today's calendar events", err);
149
+ return { date: formatDate(/* @__PURE__ */ new Date()), day_of_week: getDayOfWeek(/* @__PURE__ */ new Date()), events: [], count: 0 };
150
+ }
151
+ },
152
+ async getUpcoming(days = 7) {
153
+ try {
154
+ const start = /* @__PURE__ */ new Date();
155
+ const end = addDays(start, Math.min(days, 14));
156
+ const calendarIds = await getCalendarIds();
157
+ const events = await ExpoCalendar.getEventsAsync(calendarIds, start, end);
158
+ return {
159
+ from: formatDate(start),
160
+ days_ahead: days,
161
+ events: events.map(formatCalendarEvent),
162
+ count: events.length
163
+ };
164
+ } catch (err) {
165
+ log.error("Failed to get upcoming calendar events", err);
166
+ return { from: formatDate(/* @__PURE__ */ new Date()), days_ahead: days, events: [], count: 0 };
167
+ }
168
+ },
169
+ async create(event) {
170
+ try {
171
+ const calendarId = await getDefaultCalendarId();
172
+ if (!calendarId) throw new Error("No default calendar found");
173
+ const eventId = await ExpoCalendar.createEventAsync(calendarId, {
174
+ title: event.title,
175
+ startDate: parseDateTime(event.date, event.start_time),
176
+ endDate: parseDateTime(event.date, event.end_time || event.start_time),
177
+ location: event.location,
178
+ notes: event.notes
179
+ });
180
+ return { event_id: eventId };
181
+ } catch (err) {
182
+ log.error("Failed to create calendar event", err);
183
+ throw err;
184
+ }
185
+ },
186
+ async reschedule(params) {
187
+ try {
188
+ const updates = {};
189
+ if (params.new_date && params.new_start_time) {
190
+ updates.startDate = parseDateTime(params.new_date, params.new_start_time);
191
+ }
192
+ if (params.new_date && params.new_end_time) {
193
+ updates.endDate = parseDateTime(params.new_date, params.new_end_time);
194
+ }
195
+ await ExpoCalendar.updateEventAsync(params.event_id, updates);
196
+ return { ok: true };
197
+ } catch (err) {
198
+ log.error("Failed to reschedule event", err);
199
+ throw err;
200
+ }
201
+ },
202
+ async cancel(event_id) {
203
+ try {
204
+ await ExpoCalendar.deleteEventAsync(event_id);
205
+ return { ok: true };
206
+ } catch (err) {
207
+ log.error("Failed to cancel event", err);
208
+ throw err;
209
+ }
210
+ }
211
+ },
212
+ contacts: {
213
+ async search(query) {
214
+ try {
215
+ const { data } = await ExpoContacts.getContactsAsync({
216
+ fields: [
217
+ ExpoContacts.Fields.PhoneNumbers,
218
+ ExpoContacts.Fields.Emails,
219
+ ExpoContacts.Fields.Birthday,
220
+ ExpoContacts.Fields.Note,
221
+ ExpoContacts.Fields.Relationships
222
+ ]
223
+ });
224
+ const q = query.toLowerCase();
225
+ const filtered = data.filter(
226
+ (c) => c.name?.toLowerCase().includes(q) || c.note?.toLowerCase().includes(q)
227
+ );
228
+ return { contacts: filtered.map(formatContact), count: filtered.length };
229
+ } catch (err) {
230
+ log.error("Failed to search contacts", err);
231
+ return { contacts: [], count: 0 };
232
+ }
233
+ },
234
+ async get(idOrName) {
235
+ try {
236
+ const { data } = await ExpoContacts.getContactsAsync({
237
+ fields: [
238
+ ExpoContacts.Fields.PhoneNumbers,
239
+ ExpoContacts.Fields.Emails,
240
+ ExpoContacts.Fields.Birthday,
241
+ ExpoContacts.Fields.Note
242
+ ]
243
+ });
244
+ const contact = data.find((c) => c.id === idOrName || c.name === idOrName);
245
+ return contact ? formatContact(contact) : null;
246
+ } catch (err) {
247
+ log.error("Failed to get contact", err);
248
+ return null;
249
+ }
250
+ },
251
+ async getAll() {
252
+ try {
253
+ const { data } = await ExpoContacts.getContactsAsync({
254
+ fields: [
255
+ ExpoContacts.Fields.PhoneNumbers,
256
+ ExpoContacts.Fields.Emails,
257
+ ExpoContacts.Fields.Birthday,
258
+ ExpoContacts.Fields.Note,
259
+ ExpoContacts.Fields.Relationships
260
+ ],
261
+ sort: ExpoContacts.SortTypes.LastName
262
+ });
263
+ return { contacts: data.map(formatContact), count: data.length };
264
+ } catch (err) {
265
+ log.error("Failed to get all contacts", err);
266
+ return { contacts: [], count: 0 };
267
+ }
268
+ },
269
+ async birthdays(days = 30) {
270
+ try {
271
+ const { data } = await ExpoContacts.getContactsAsync({
272
+ fields: [ExpoContacts.Fields.Birthday]
273
+ });
274
+ const today = /* @__PURE__ */ new Date();
275
+ const futureDate = addDays(today, days);
276
+ const birthdays = data.filter((c) => c.birthday).map((c) => {
277
+ const birthday = c.birthday;
278
+ const thisYearBday = new Date(today.getFullYear(), birthday.month - 1, birthday.day);
279
+ if (thisYearBday < today) {
280
+ thisYearBday.setFullYear(today.getFullYear() + 1);
281
+ }
282
+ return {
283
+ name: c.name,
284
+ relationship: c.relationships?.[0]?.relationship || "contact",
285
+ birthday: `${birthday.year || "????"}-${String(birthday.month).padStart(2, "0")}-${String(birthday.day).padStart(2, "0")}`,
286
+ days_until: Math.floor((thisYearBday.getTime() - today.getTime()) / (1e3 * 60 * 60 * 24)),
287
+ is_today: thisYearBday.toDateString() === today.toDateString()
288
+ };
289
+ }).filter((b) => b.days_until >= 0 && b.days_until <= days);
290
+ return { birthdays, count: birthdays.length };
291
+ } catch (err) {
292
+ log.error("Failed to get birthdays", err);
293
+ return { birthdays: [], count: 0 };
294
+ }
295
+ }
296
+ },
297
+ location: {
298
+ async getCurrent() {
299
+ try {
300
+ const location = await ExpoLocation.getCurrentPositionAsync({
301
+ accuracy: ExpoLocation.Accuracy.Balanced
302
+ });
303
+ const [geo] = await ExpoLocation.reverseGeocodeAsync({
304
+ latitude: location.coords.latitude,
305
+ longitude: location.coords.longitude
306
+ });
307
+ return {
308
+ lat: location.coords.latitude,
309
+ lng: location.coords.longitude,
310
+ place: formatPlace(geo),
311
+ address: formatAddress(geo),
312
+ type: classifyLocation(geo),
313
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
314
+ };
315
+ } catch (err) {
316
+ log.error("Failed to get current location", err);
317
+ throw err;
318
+ }
319
+ }
320
+ },
321
+ battery: {
322
+ async getStatus() {
323
+ try {
324
+ const [level, state, lowPower] = await Promise.all([
325
+ ExpoBattery.getBatteryLevelAsync(),
326
+ ExpoBattery.getBatteryStateAsync(),
327
+ ExpoBattery.isLowPowerModeEnabledAsync()
328
+ ]);
329
+ return {
330
+ ok: true,
331
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
332
+ battery: {
333
+ level: Math.round(level * 100),
334
+ state: mapBatteryState(state),
335
+ low_power_mode: lowPower
336
+ }
337
+ };
338
+ } catch (err) {
339
+ log.error("Failed to get battery status", err);
340
+ throw err;
341
+ }
342
+ }
343
+ },
344
+ network: {
345
+ async getStatus() {
346
+ try {
347
+ const state = await ExpoNetwork.getNetworkStateAsync();
348
+ return {
349
+ type: state.type === ExpoNetwork.NetworkStateType.WIFI ? "wifi" : state.type === ExpoNetwork.NetworkStateType.CELLULAR ? "cellular" : "none",
350
+ is_connected: state.isConnected ?? false,
351
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
352
+ };
353
+ } catch (err) {
354
+ log.error("Failed to get network status", err);
355
+ throw err;
356
+ }
357
+ }
358
+ },
359
+ notifications: {
360
+ async list() {
361
+ try {
362
+ const presented = await ExpoNotifications.getPresentedNotificationsAsync();
363
+ const notifications = presented.map(formatNotification);
364
+ return {
365
+ notifications,
366
+ total: notifications.length,
367
+ unread: notifications.filter((n) => !n.read).length
368
+ };
369
+ } catch (err) {
370
+ log.error("Failed to get notifications", err);
371
+ return { notifications: [], total: 0, unread: 0 };
372
+ }
373
+ }
374
+ },
375
+ sms: {
376
+ async isAvailable() {
377
+ try {
378
+ return await ExpoSMS.isAvailableAsync();
379
+ } catch (err) {
380
+ log.error("Failed to check SMS availability", err);
381
+ return false;
382
+ }
383
+ },
384
+ async send(phone, message) {
385
+ try {
386
+ const { result } = await ExpoSMS.sendSMSAsync([phone], message);
387
+ return {
388
+ ok: result === "sent" || result === "unknown",
389
+ status: result
390
+ };
391
+ } catch (err) {
392
+ log.error("Failed to send SMS", err);
393
+ throw err;
394
+ }
395
+ }
396
+ },
397
+ maps: {
398
+ async navigate(destination, mode = "driving") {
399
+ try {
400
+ const encoded = encodeURIComponent(destination);
401
+ const url = Platform.OS === "ios" ? `maps://maps.apple.com/?daddr=${encoded}&dirflg=${mode[0]}` : `google.navigation:q=${encoded}&mode=${mode}`;
402
+ const canOpen = await ExpoLinking.canOpenURL(url);
403
+ if (!canOpen) {
404
+ const fallbackUrl = `https://www.google.com/maps/search/${encoded}`;
405
+ await ExpoLinking.openURL(fallbackUrl);
406
+ } else {
407
+ await ExpoLinking.openURL(url);
408
+ }
409
+ return { ok: true, status: "navigating" };
410
+ } catch (err) {
411
+ log.error("Failed to navigate", err);
412
+ return { ok: false, status: "error" };
413
+ }
414
+ }
415
+ },
416
+ http: {
417
+ async get(url, headers = {}) {
418
+ try {
419
+ const res = await fetch(url, { headers });
420
+ const data = await res.json();
421
+ return {
422
+ ok: res.ok,
423
+ status: res.status,
424
+ data,
425
+ headers: Object.fromEntries(res.headers.entries())
426
+ };
427
+ } catch (err) {
428
+ log.error("HTTP GET failed", err);
429
+ throw err;
430
+ }
431
+ },
432
+ async post(url, body, headers = {}) {
433
+ try {
434
+ const res = await fetch(url, {
435
+ method: "POST",
436
+ headers: { "Content-Type": "application/json", ...headers },
437
+ body: JSON.stringify(body)
438
+ });
439
+ const data = await res.json();
440
+ return {
441
+ ok: res.ok,
442
+ status: res.status,
443
+ data,
444
+ headers: Object.fromEntries(res.headers.entries())
445
+ };
446
+ } catch (err) {
447
+ log.error("HTTP POST failed", err);
448
+ throw err;
449
+ }
450
+ },
451
+ async put(url, body, headers = {}) {
452
+ try {
453
+ const res = await fetch(url, {
454
+ method: "PUT",
455
+ headers: { "Content-Type": "application/json", ...headers },
456
+ body: JSON.stringify(body)
457
+ });
458
+ const data = await res.json();
459
+ return {
460
+ ok: res.ok,
461
+ status: res.status,
462
+ data,
463
+ headers: Object.fromEntries(res.headers.entries())
464
+ };
465
+ } catch (err) {
466
+ log.error("HTTP PUT failed", err);
467
+ throw err;
468
+ }
469
+ },
470
+ async delete(url, body, headers = {}) {
471
+ try {
472
+ const opts = {
473
+ method: "DELETE",
474
+ headers: { "Content-Type": "application/json", ...headers }
475
+ };
476
+ if (body) opts.body = JSON.stringify(body);
477
+ const res = await fetch(url, opts);
478
+ const data = await res.json();
479
+ return {
480
+ ok: res.ok,
481
+ status: res.status,
482
+ data,
483
+ headers: Object.fromEntries(res.headers.entries())
484
+ };
485
+ } catch (err) {
486
+ log.error("HTTP DELETE failed", err);
487
+ throw err;
488
+ }
489
+ }
490
+ },
491
+ storage: {
492
+ async get(key) {
493
+ try {
494
+ return await AsyncStorage.getItem(storagePrefix + key);
495
+ } catch (err) {
496
+ log.error(`Failed to get storage key: ${key}`, err);
497
+ return null;
498
+ }
499
+ },
500
+ async set(key, value) {
501
+ try {
502
+ await AsyncStorage.setItem(storagePrefix + key, value);
503
+ } catch (err) {
504
+ log.error(`Failed to set storage key: ${key}`, err);
505
+ }
506
+ },
507
+ async delete(key) {
508
+ try {
509
+ await AsyncStorage.removeItem(storagePrefix + key);
510
+ } catch (err) {
511
+ log.error(`Failed to delete storage key: ${key}`, err);
512
+ }
513
+ },
514
+ async keys() {
515
+ try {
516
+ const allKeys = await AsyncStorage.getAllKeys();
517
+ return allKeys.filter((k) => k.startsWith(storagePrefix)).map((k) => k.slice(storagePrefix.length));
518
+ } catch (err) {
519
+ log.error("Failed to list storage keys", err);
520
+ return [];
521
+ }
522
+ }
523
+ },
524
+ permissions: {
525
+ async check(capability) {
526
+ try {
527
+ switch (capability) {
528
+ case "calendar":
529
+ const cal = await ExpoCalendar.getCalendarPermissionsAsync();
530
+ return cal.status === "granted" ? "granted" : cal.status === "denied" ? "denied" : "not_requested";
531
+ case "contacts":
532
+ const con = await ExpoContacts.getPermissionsAsync();
533
+ return con.status === "granted" ? "granted" : con.status === "denied" ? "denied" : "not_requested";
534
+ case "location":
535
+ const loc = await ExpoLocation.getForegroundPermissionsAsync();
536
+ return loc.status === "granted" ? "granted" : loc.status === "denied" ? "denied" : "not_requested";
537
+ case "notifications":
538
+ const notif = await ExpoNotifications.getPermissionsAsync();
539
+ return notif.granted ? "granted" : notif.canAskAgain ? "not_requested" : "denied";
540
+ default:
541
+ return "not_requested";
542
+ }
543
+ } catch (err) {
544
+ log.error(`Failed to check permission: ${capability}`, err);
545
+ return "denied";
546
+ }
547
+ },
548
+ async request(capability) {
549
+ try {
550
+ switch (capability) {
551
+ case "calendar":
552
+ const cal = await ExpoCalendar.requestCalendarPermissionsAsync();
553
+ return cal.status === "granted";
554
+ case "contacts":
555
+ const con = await ExpoContacts.requestPermissionsAsync();
556
+ return con.status === "granted";
557
+ case "location":
558
+ const loc = await ExpoLocation.requestForegroundPermissionsAsync();
559
+ return loc.status === "granted";
560
+ case "notifications":
561
+ const notif = await ExpoNotifications.requestPermissionsAsync();
562
+ return notif.granted;
563
+ default:
564
+ return false;
565
+ }
566
+ } catch (err) {
567
+ log.error(`Failed to request permission: ${capability}`, err);
568
+ return false;
569
+ }
570
+ },
571
+ async getAll() {
572
+ try {
573
+ const [cal, con, loc, notif] = await Promise.allSettled([
574
+ ExpoCalendar.getCalendarPermissionsAsync(),
575
+ ExpoContacts.getPermissionsAsync(),
576
+ ExpoLocation.getForegroundPermissionsAsync(),
577
+ ExpoNotifications.getPermissionsAsync()
578
+ ]);
579
+ return {
580
+ calendar: cal.status === "fulfilled" ? cal.value.status === "granted" ? "granted" : cal.value.status === "denied" ? "denied" : "not_requested" : "denied",
581
+ contacts: con.status === "fulfilled" ? con.value.status === "granted" ? "granted" : con.value.status === "denied" ? "denied" : "not_requested" : "denied",
582
+ location: loc.status === "fulfilled" ? loc.value.status === "granted" ? "granted" : loc.value.status === "denied" ? "denied" : "not_requested" : "denied",
583
+ notifications: notif.status === "fulfilled" ? notif.value.granted ? "granted" : notif.value.canAskAgain ? "not_requested" : "denied" : "denied"
584
+ };
585
+ } catch (err) {
586
+ log.error("Failed to get all permissions", err);
587
+ return { calendar: "denied", contacts: "denied", location: "denied", notifications: "denied" };
588
+ }
589
+ }
590
+ },
591
+ device: {
592
+ getDeviceId() {
593
+ return deviceId;
594
+ },
595
+ getPlatform() {
596
+ return {
597
+ os: Platform.OS,
598
+ version: Platform.Version ? String(Platform.Version) : "unknown"
599
+ };
600
+ }
601
+ },
602
+ log
603
+ };
604
+ return bridge;
605
+ }
606
+ export {
607
+ createBridge
608
+ };
609
+ //# sourceMappingURL=createBridge.mjs.map