@robono/messaging-react-native 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,8 @@
1
- ## 0.7.0 — unreleased
1
+ ## 0.8.0
2
+
3
+ Connect the supplied Expo Messenger to native incoming calls automatically. Register VoIP/system calling with the existing signed-in device, clear account state on sign-out, cache contact names and use selected Android calling appearance. Requires calling-react-native 0.6.0 and a native app rebuild.
4
+
5
+ ## 0.7.0 — 2026-11-04
2
6
 
3
7
  Supply automatic Expo calling, Maps and connected chat extras. Use Messenger attachment menus, grouped media, bounded dialogs and anchored inbox actions. Connect managed call alerts, unanswered-call messages and call-aware media audio. Stop location and pending uploads during account cleanup.
4
8
 
package/SBOM.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@robono/messaging-react-native@0.7.0",
6
- "documentNamespace": "https://robono.com/sbom/messaging-react-native/0.7.0/64a38b3600c45aeb4fe86fad2ea3b1891a588c20",
5
+ "name": "@robono/messaging-react-native@0.8.0",
6
+ "documentNamespace": "https://robono.com/sbom/messaging-react-native/0.8.0/cf47b415af23537ac6bf082eb5241714a556066f",
7
7
  "creationInfo": {
8
- "created": "2026-11-04T11:52:06.365Z",
8
+ "created": "2026-11-04T14:48:48.955Z",
9
9
  "creators": [
10
10
  "Organization: Robono (support@robono.com)"
11
11
  ]
@@ -14,11 +14,11 @@
14
14
  {
15
15
  "name": "@robono/messaging-react-native",
16
16
  "SPDXID": "SPDXRef-Package",
17
- "versionInfo": "0.7.0",
17
+ "versionInfo": "0.8.0",
18
18
  "downloadLocation": "NOASSERTION",
19
19
  "filesAnalyzed": true,
20
20
  "packageVerificationCode": {
21
- "packageVerificationCodeValue": "64a38b3600c45aeb4fe86fad2ea3b1891a588c20"
21
+ "packageVerificationCodeValue": "cf47b415af23537ac6bf082eb5241714a556066f"
22
22
  },
23
23
  "licenseConcluded": "LicenseRef-RobonoSDK",
24
24
  "licenseDeclared": "LicenseRef-RobonoSDK",
@@ -26,7 +26,7 @@
26
26
  "LicenseRef-RobonoSDK"
27
27
  ],
28
28
  "copyrightText": "Copyright (c) 2026 Add to Loop LLC.",
29
- "comment": "Host-supplied peers (not bundled): {\"@robono/messaging\":\"^0.5.0\",\"react\":\">=18.2 <20\",\"react-native\":\">=0.76 <1\",\"expo-audio\":\">=55 <57\",\"expo-image-picker\":\">=55 <57\",\"expo-document-picker\":\">=55 <57\",\"expo-file-system\":\">=55.0.5 <57\",\"expo-sharing\":\">=55 <57\",\"expo-video\":\">=55 <57\",\"expo-crypto\":\">=55 <57\",\"react-native-safe-area-context\":\">=5 <6\",\"react-native-svg\":\">=15 <16\",\"@robono/calling-react-native\":\"^0.5.0\",\"expo-clipboard\":\">=8 <57\",\"expo-image-manipulator\":\">=14 <57\",\"expo-video-thumbnails\":\">=14 <57\",\"react-native-webview\":\">=13 <14\",\"expo-blur\":\">=55 <57\",\"expo-notifications\":\">=55 <57\",\"expo-secure-store\":\">=55 <57\",\"@robono/location-react-native\":\"^0.2.0\",\"expo-location\":\">=55 <57\",\"expo-constants\":\">=55 <57\",\"@robono/location\":\"^0.2.0\",\"expo-haptics\":\">=55 <57\",\"@robono/calling\":\"^0.4.0\"}"
29
+ "comment": "Host-supplied peers (not bundled): {\"@robono/messaging\":\"^0.5.0\",\"react\":\">=18.2 <20\",\"react-native\":\">=0.76 <1\",\"expo-audio\":\">=55 <57\",\"expo-image-picker\":\">=55 <57\",\"expo-document-picker\":\">=55 <57\",\"expo-file-system\":\">=55.0.5 <57\",\"expo-sharing\":\">=55 <57\",\"expo-video\":\">=55 <57\",\"expo-crypto\":\">=55 <57\",\"react-native-safe-area-context\":\">=5 <6\",\"react-native-svg\":\">=15 <16\",\"@robono/calling-react-native\":\"^0.6.0\",\"expo-clipboard\":\">=8 <57\",\"expo-image-manipulator\":\">=14 <57\",\"expo-video-thumbnails\":\">=14 <57\",\"react-native-webview\":\">=13 <14\",\"expo-blur\":\">=55 <57\",\"expo-notifications\":\">=55 <57\",\"expo-secure-store\":\">=55 <57\",\"@robono/location-react-native\":\"^0.2.0\",\"expo-location\":\">=55 <57\",\"expo-constants\":\">=55 <57\",\"@robono/location\":\"^0.2.0\",\"expo-haptics\":\">=55 <57\",\"@robono/calling\":\"^0.4.0\"}"
30
30
  }
31
31
  ],
32
32
  "files": [
@@ -36,11 +36,11 @@
36
36
  "checksums": [
37
37
  {
38
38
  "algorithm": "SHA256",
39
- "checksumValue": "9fee86c6f9f22b450f3401eed5e8c6f3a19cc838f8ae75ef5313975695b65a77"
39
+ "checksumValue": "e55d7df1432d4b79a13136b8fd1a353e1c9bc27a9d0f99867e226bec143549c1"
40
40
  },
41
41
  {
42
42
  "algorithm": "SHA1",
43
- "checksumValue": "eb1d3625752e3ac80d1b8d61602da2bee48d844a"
43
+ "checksumValue": "6770a5e98970ee51ed63a1983b0fcc0d8ce35ff1"
44
44
  }
45
45
  ],
46
46
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -378,11 +378,11 @@
378
378
  "checksums": [
379
379
  {
380
380
  "algorithm": "SHA256",
381
- "checksumValue": "8b4ef9b119210d7fc782a43d11312d7c766d90eaf239e32ffa0c376e696ff00c"
381
+ "checksumValue": "c5bed28fd78ae8b05fccffa938f6b846b8577152e5eb400c96e13869f44d9b80"
382
382
  },
383
383
  {
384
384
  "algorithm": "SHA1",
385
- "checksumValue": "d2b070f68cafada3481106506c8c44600415a6e4"
385
+ "checksumValue": "273d74b211a3ba24a9d332cff8d1715760b67c67"
386
386
  }
387
387
  ],
388
388
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -397,11 +397,11 @@
397
397
  "checksums": [
398
398
  {
399
399
  "algorithm": "SHA256",
400
- "checksumValue": "a36cfba08f5ca16d2d0c84384374ff0558edd3a0453df8bd48029266cd8ef2e3"
400
+ "checksumValue": "044c993f1e8054cff33de875a65a46fd028e0a2d54e8bdafa3f543843bd89e06"
401
401
  },
402
402
  {
403
403
  "algorithm": "SHA1",
404
- "checksumValue": "c7bc75d7bde186715708eaa934a039ed718abc99"
404
+ "checksumValue": "61ee879a73a4aca1be59cc71df0fdcd307c4db35"
405
405
  }
406
406
  ],
407
407
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -568,11 +568,11 @@
568
568
  "checksums": [
569
569
  {
570
570
  "algorithm": "SHA256",
571
- "checksumValue": "32a87fccc2a6a0a990dbb4ed7af66ea7ba31a4e8f34d8be3db2d5ce775fb7128"
571
+ "checksumValue": "59160db58c4b5b958dfb97d4cff8f9d6b6d221fc004c1de688dedf5f7561da4c"
572
572
  },
573
573
  {
574
574
  "algorithm": "SHA1",
575
- "checksumValue": "c649ea8593ea27c8de09e524ddb4d1d722cff7e7"
575
+ "checksumValue": "20547cd95b9d1afd7e1a75ba2ba0159a08d5690b"
576
576
  }
577
577
  ],
578
578
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -587,11 +587,11 @@
587
587
  "checksums": [
588
588
  {
589
589
  "algorithm": "SHA256",
590
- "checksumValue": "9b90e0a3effb17c3c9b7dc920c608aa5dca782889a6211382e12ea7cacb3f552"
590
+ "checksumValue": "4b3238a3cce45c5bd44f23c3bb78ef26d20a00db57604eb8f480495bc8531dd8"
591
591
  },
592
592
  {
593
593
  "algorithm": "SHA1",
594
- "checksumValue": "3098a4c8271cf6e1458e9bb05e62252d11bd5dc7"
594
+ "checksumValue": "f040d289da1effc56ec1ef7e8b1fd68b5e37e670"
595
595
  }
596
596
  ],
597
597
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -606,11 +606,11 @@
606
606
  "checksums": [
607
607
  {
608
608
  "algorithm": "SHA256",
609
- "checksumValue": "949f4014ea5154a3593a01659efb350d5ff5f560a6c099619e99494fbab7e3e6"
609
+ "checksumValue": "3a050d3ffedfaa28fbc5015f42a2cc0807ebbbb0448cb1401a27a29447e11d5a"
610
610
  },
611
611
  {
612
612
  "algorithm": "SHA1",
613
- "checksumValue": "fb174e9ba390c13c2d8a08e43cb7bb157fbd1a67"
613
+ "checksumValue": "15be46b0a592f44f3083157c5c6ae544e930eb06"
614
614
  }
615
615
  ],
616
616
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -625,11 +625,11 @@
625
625
  "checksums": [
626
626
  {
627
627
  "algorithm": "SHA256",
628
- "checksumValue": "0ef434b1b812b8e0ae4ad1a7637a3591bb85813212f00357085f114f144b1a7a"
628
+ "checksumValue": "e71ab828d444ac6b5ceba26ff3cffd20db860cb2d0c2e07c00d7fcb787db145c"
629
629
  },
630
630
  {
631
631
  "algorithm": "SHA1",
632
- "checksumValue": "1e1f912b1e361c0bff383cf68027b9fb5becbb28"
632
+ "checksumValue": "f9aafd3f224bc14d210197b1d81f4af3b3824bf8"
633
633
  }
634
634
  ],
635
635
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -2525,11 +2525,11 @@
2525
2525
  "checksums": [
2526
2526
  {
2527
2527
  "algorithm": "SHA256",
2528
- "checksumValue": "a74bf100a7e62148891eaa2f5e6c84b0c3152f441a513ff48da948fe7bba2ceb"
2528
+ "checksumValue": "043c6b2cce5c63140f56a7ebcdfc2b577811a910ec38449ba56a2f7cd46867a9"
2529
2529
  },
2530
2530
  {
2531
2531
  "algorithm": "SHA1",
2532
- "checksumValue": "9dbc298f88a386db7cbc0002fba6da2cb36d9a09"
2532
+ "checksumValue": "50a3a3fa9883b5eb33c39ba369ad44d6dac09a90"
2533
2533
  }
2534
2534
  ],
2535
2535
  "licenseConcluded": "LicenseRef-RobonoSDK",
@@ -11,6 +11,9 @@ export declare function createExpoMessengerCalling(options: {
11
11
  }>;
12
12
  inbox: MessagingInboxClient;
13
13
  }): {
14
+ systemNotifications: import("@robono/calling-react-native/system-calling").SystemCallNotifications;
15
+ notification(data: unknown, opened?: boolean): Promise<boolean>;
16
+ dispose(): Promise<void>;
14
17
  updateContacts(next: Record<string, {
15
18
  name: string;
16
19
  avatarUrl?: string;
@@ -19,11 +22,10 @@ export declare function createExpoMessengerCalling(options: {
19
22
  getSnapshot: () => MessengerCallingState;
20
23
  subscribe(fn: () => void): () => void;
21
24
  isActive: () => boolean;
25
+ reportError(message: string): void;
22
26
  dismissUnanswered(): void;
23
- notification(data: unknown, opened?: boolean): Promise<boolean>;
24
27
  prime(): void;
25
28
  call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
26
- dispose(): Promise<void>;
27
29
  };
28
30
  export declare function useExpoMessengerCalling(calling?: MessengerCalling): MessengerCallingState;
29
31
  export declare function ExpoMessengerCalls({ calling, state }: {
@@ -5,12 +5,14 @@ import { setAudioModeAsync } from 'expo-audio';
5
5
  import { createMessengerCalling } from '@robono/calling-react-native/messenger';
6
6
  import { createNativeCallAdapters } from '@robono/calling-react-native/native';
7
7
  import { createNativeCallRuntime } from '@robono/calling-react-native/runtime';
8
+ import { createNativeSystemCalling } from '@robono/calling-react-native/system-calling';
8
9
  import { NativeCallingOverlay, NativeGroupCallingOverlay } from '@robono/calling-react-native/native-video';
9
10
  import { createMessagingTransport } from '@robono/messaging';
10
11
  export function createExpoMessengerCalling(options) {
11
12
  const url = new URL(options.endpoint);
12
13
  url.searchParams.set('service', 'calling');
13
14
  let people = {};
15
+ const system = createNativeSystemCalling({ start: () => calling.start(), onError: () => calling.reportError('The incoming call could not connect. Open the app and check microphone access and your connection.') });
14
16
  const calling = createMessengerCalling({
15
17
  request: createMessagingTransport({ ...options, url: url.href }),
16
18
  resolveContact: id => {
@@ -22,12 +24,22 @@ export function createExpoMessengerCalling(options) {
22
24
  },
23
25
  resolveMember: (_app, id) => { const person = (options.people?.() ?? people)[id]; return person ? { name: person.name, avatarUri: person.avatarUrl } : null; },
24
26
  adapters: appearance => {
27
+ system.updateAppearance(appearance);
28
+ let controller;
25
29
  const runtime = createNativeCallRuntime({ notificationTitle: 'Call in progress', notificationText: 'Tap to return to your call', channelName: 'Calls', endLabel: 'End call', onError: () => { } });
26
- const adapter = createNativeCallAdapters({ appearance, runtime, ringtone: 'robono', onError: () => { }, restoreAudio: () => setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, shouldPlayInBackground: false, shouldRouteThroughEarpiece: false, interruptionMode: 'mixWithOthers' }) });
27
- return { ...adapter, attach: runtime.attach, dispose: runtime.dispose };
30
+ const adapter = createNativeCallAdapters({ appearance, runtime, systemCalling: system.audio(() => controller), ringtone: 'robono', onError: () => { }, restoreAudio: () => setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, shouldPlayInBackground: false, shouldRouteThroughEarpiece: false, interruptionMode: 'mixWithOthers' }) });
31
+ return { ...adapter, attach(value) { controller = value; const a = runtime.attach(value), b = system.attach(value); return () => { b(); a(); controller = undefined; }; }, dispose: runtime.dispose };
28
32
  },
29
33
  });
30
- return { ...calling, updateContacts(next) { people = next; } };
34
+ return { ...calling, systemNotifications: system.notifications,
35
+ async notification(data, opened = false) { await system.receive(data); return calling.notification(data, opened); },
36
+ async dispose() { await system.dispose(); await calling.dispose(); },
37
+ updateContacts(next) { people = next; const names = {}; for (const row of options.inbox.getSnapshot().conversations) {
38
+ const person = next[row.contactId ?? ''];
39
+ const name = row.kind === 'group' ? row.name : person?.name;
40
+ if (name)
41
+ names[row.id] = name;
42
+ } system.cacheContacts(names); } };
31
43
  }
32
44
  const absent = { direct: null, group: null, loading: false, error: '', features: { voice: false, video: false } };
33
45
  const noSubscribe = () => () => { };
@@ -38,6 +38,9 @@ export declare function createExpoMessenger(options: {
38
38
  enable(): Promise<void>;
39
39
  } | undefined;
40
40
  calling: {
41
+ systemNotifications: import("@robono/calling-react-native/system-calling").SystemCallNotifications;
42
+ notification(data: unknown, opened?: boolean): Promise<boolean>;
43
+ dispose(): Promise<void>;
41
44
  updateContacts(next: Record<string, {
42
45
  name: string;
43
46
  avatarUrl?: string;
@@ -46,11 +49,10 @@ export declare function createExpoMessenger(options: {
46
49
  getSnapshot: () => import("@robono/calling-react-native/messenger").MessengerCallingState;
47
50
  subscribe(fn: () => void): () => void;
48
51
  isActive: () => boolean;
52
+ reportError(message: string): void;
49
53
  dismissUnanswered(): void;
50
- notification(data: unknown, opened?: boolean): Promise<boolean>;
51
54
  prime(): void;
52
55
  call(conversationId: string, kind: "voice" | "video", group?: boolean): Promise<void>;
53
- dispose(): Promise<void>;
54
56
  } | undefined;
55
57
  maps: LocationWorkspace | undefined;
56
58
  };
@@ -49,7 +49,7 @@ export function createExpoMessenger(options) {
49
49
  catch {
50
50
  return false;
51
51
  } },
52
- onCall: calling?.notification, open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
52
+ onCall: calling?.notification, systemCalls: calling?.systemNotifications, open: e => open(e.conversationId, e.messageId), refresh: () => inbox.refresh().then(() => { }), onError: () => { },
53
53
  });
54
54
  if (closed) {
55
55
  await n.signOut();
@@ -1,4 +1,5 @@
1
1
  import * as Notifications from 'expo-notifications';
2
+ import type { SystemCallNotifications } from '@robono/calling-react-native/system-calling';
2
3
  import { type MessagingNotification } from '@robono/messaging';
3
4
  export interface ExpoMessagingNotificationOptions {
4
5
  /** The combined server route, with service=notifications. */
@@ -11,6 +12,7 @@ export interface ExpoMessagingNotificationOptions {
11
12
  onError: (error: Error) => void;
12
13
  fetch?: typeof fetch;
13
14
  onCall?: (data: unknown, opened: boolean) => Promise<boolean>;
15
+ systemCalls?: SystemCallNotifications;
14
16
  }
15
17
  /** One restored account. Never prompts on mount, replaces a global handler or opens an unverified call. */
16
18
  export declare function connectExpoMessagingNotifications(options: ExpoMessagingNotificationOptions): Promise<{
@@ -10,7 +10,7 @@ export async function connectExpoMessagingNotifications(options) {
10
10
  const endpoint = new URL(options.url);
11
11
  if (endpoint.protocol !== 'https:' || endpoint.username || endpoint.password)
12
12
  throw Error('Use your authenticated HTTPS notification endpoint.');
13
- let disposed = false, closing = false, registerQueue = Promise.resolve(), accountScope;
13
+ let disposed = false, closing = false, registerQueue = Promise.resolve(), accountScope, lastToken;
14
14
  const key = 'robono.messaging.device.' + await Crypto.digestStringAsync(Crypto.CryptoDigestAlgorithm.SHA256, endpoint.origin + endpoint.pathname);
15
15
  let deviceId = await SecureStore.getItemAsync(key);
16
16
  if (!deviceId) {
@@ -33,16 +33,21 @@ export async function connectExpoMessagingNotifications(options) {
33
33
  accountScope = status.accountScope;
34
34
  const router = createMessagingNotificationRouter({ accountScope: status.accountScope, authorize: options.authorize, open: options.open, refresh: options.refresh });
35
35
  const register = (token) => {
36
+ lastToken = token;
36
37
  const work = registerQueue.then(async () => {
37
38
  if (disposed || closing)
38
39
  return;
39
- const data = await request({ operation: 'register-device', calling: !!options.onCall, deviceId, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', token: String(token.data) });
40
+ const system = await options.systemCalls?.registration();
41
+ const data = await request({ operation: 'register-device', calling: !!options.onCall, ...system, deviceId, provider: Platform.OS === 'ios' ? 'apns' : 'fcm', token: String(token.data) });
40
42
  if (data.accountScope !== status.accountScope)
41
43
  throw Error('The signed-in account changed. Reconnect notifications for the new account.');
42
44
  });
43
45
  registerQueue = work.catch(() => { });
44
46
  return work;
45
47
  };
48
+ if (options.systemCalls)
49
+ await options.systemCalls.configure(status.accountScope, () => { if (lastToken && !closing && !disposed)
50
+ void register(lastToken).catch(options.onError); });
46
51
  const tokenListener = Notifications.addPushTokenListener(token => { void register(token).catch(options.onError); });
47
52
  const handle = async (data, opened = false) => { if (!data || disposed || closing)
48
53
  return false; if (data.accountScope === accountScope && ['calling.incoming', 'calling.dismissed', 'calling.group_incoming', 'calling.group_dismissed'].includes(String(data.event)) && options.onCall) {
@@ -74,7 +79,7 @@ export async function connectExpoMessagingNotifications(options) {
74
79
  await response(await Notifications.getLastNotificationResponseAsync());
75
80
  })();
76
81
  void ready.catch(options.onError);
77
- const dispose = () => { disposed = true; router.dispose(); tokenListener.remove(); received.remove(); opened.remove(); };
82
+ const dispose = () => { disposed = true; router.dispose(); tokenListener.remove(); received.remove(); opened.remove(); void options.systemCalls?.disconnect().catch(options.onError); };
78
83
  return {
79
84
  ready,
80
85
  /** Call only after the user chooses Enable notifications in your interface. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robono/messaging-react-native",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Optional, composable direct and group messaging screens for Robono.",
@@ -65,7 +65,7 @@
65
65
  "expo-crypto": ">=55 <57",
66
66
  "react-native-safe-area-context": ">=5 <6",
67
67
  "react-native-svg": ">=15 <16",
68
- "@robono/calling-react-native": "^0.5.0",
68
+ "@robono/calling-react-native": "^0.6.0",
69
69
  "expo-clipboard": ">=8 <57",
70
70
  "expo-image-manipulator": ">=14 <57",
71
71
  "expo-video-thumbnails": ">=14 <57",