@nyaruka/temba-components 0.171.0 → 0.172.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/dist/temba-components.js +1015 -747
  3. package/dist/temba-components.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/RapidElement.ts +83 -41
  6. package/src/display/Button.ts +14 -2
  7. package/src/display/Chat.ts +41 -22
  8. package/src/display/Label.ts +18 -3
  9. package/src/events/eventRenderers.ts +4 -0
  10. package/src/flow/FlowSearch.ts +12 -392
  11. package/src/flow/actions/send_msg.ts +2 -1
  12. package/src/flow/actions/set_contact_field.ts +1 -0
  13. package/src/flow/actions/set_contact_name.ts +1 -0
  14. package/src/flow/nodes/split_by_ticket.ts +1 -0
  15. package/src/flow/nodes/wait_for_dial.ts +1 -0
  16. package/src/form/RangePicker.ts +10 -2
  17. package/src/form/select/Select.ts +15 -2
  18. package/src/layout/SearchModal.ts +564 -0
  19. package/src/list/BroadcastList.ts +17 -6
  20. package/src/list/ContentList.ts +29 -13
  21. package/src/list/FieldList.ts +8 -1
  22. package/src/list/TembaMenu.ts +71 -13
  23. package/src/live/CampaignEvents.ts +31 -11
  24. package/src/live/ContactChat.ts +276 -76
  25. package/src/live/ContactTimeline.ts +46 -24
  26. package/src/live/ContactWatch.ts +166 -63
  27. package/src/live/Realtime.ts +137 -8
  28. package/src/live/SocketService.ts +115 -10
  29. package/src/live/TicketSearch.ts +290 -0
  30. package/src/live/Watchers.ts +93 -0
  31. package/src/simulator/Simulator.ts +59 -36
  32. package/src/store/Store.ts +17 -34
  33. package/src/styles/designTokens.ts +57 -17
  34. package/src/styles/pillVariants.ts +42 -10
  35. package/static/css/design-system.css +47 -12
  36. package/static/css/temba-components.css +31 -9
  37. package/temba-modules.ts +2 -0
@@ -1,7 +1,19 @@
1
1
  import { Contact } from '../interfaces';
2
2
  import { Events } from '../events/eventRenderers';
3
3
  import { getStore } from '../store/Store';
4
- import { RealtimeSubscription, subscribeToContactHistory } from './Realtime';
4
+ import { Watchers } from './Watchers';
5
+ import {
6
+ ContactFieldChangedEvent,
7
+ ContactFlowChangedEvent,
8
+ ContactGroupsChangedEvent,
9
+ ContactHistoryEvent,
10
+ ContactLanguageChangedEvent,
11
+ ContactLastSeenChangedEvent,
12
+ ContactNameChangedEvent,
13
+ ContactStatusChangedEvent,
14
+ RealtimeSubscription,
15
+ subscribeToContactHistory
16
+ } from './Realtime';
5
17
 
6
18
  /**
7
19
  * Central interest registry for live contact state. Components declare what
@@ -28,7 +40,12 @@ import { RealtimeSubscription, subscribeToContactHistory } from './Realtime';
28
40
  * unsubscribes the socket subscription and cached contact are dropped.
29
41
  *
30
42
  * Wildcard watchers are event-stream consumers (e.g. chat renders history) -
31
- * they get every live event but no eventless deliveries.
43
+ * they get every live event but no eventless deliveries, and an entry with
44
+ * only those holds no contact and fetches nothing. They keep their own copy
45
+ * instead, applying events to it with applyContactEvent so how an event
46
+ * changes a contact is still defined in one place. Any watcher can pass
47
+ * onSubscribed to be told when the channel goes live, which is where a
48
+ * stream consumer catches up on what it missed while offline.
32
49
  */
33
50
 
34
51
  // same shape components previously fetched themselves - urns are expanded and
@@ -60,15 +77,19 @@ export const CONTACT_STATE_TYPES = [
60
77
  Events.CONTACT_LAST_SEEN_CHANGED
61
78
  ];
62
79
 
63
- export type ContactEventHandler = (event: any, contact: Contact) => void;
80
+ export type ContactEventHandler = (
81
+ event: ContactHistoryEvent | null,
82
+ contact: Contact
83
+ ) => void;
64
84
 
65
85
  interface Watcher {
66
86
  types: string[] | '*';
67
87
  onEvent: ContactEventHandler;
88
+ onSubscribed?: () => void;
68
89
  }
69
90
 
70
91
  interface WatchedContact {
71
- watchers: Watcher[];
92
+ watchers: Watchers<Watcher>;
72
93
  sub: RealtimeSubscription;
73
94
  contact: Contact;
74
95
  fetchSeq: number;
@@ -76,10 +97,19 @@ interface WatchedContact {
76
97
  // events landing while one is outstanding need a refetch to survive
77
98
  fetching: number;
78
99
  refetchTimer: number;
100
+ // whether the channel is live, so watchers joining an already-subscribed
101
+ // channel still get their initial catch-up call
102
+ subscribed: boolean;
79
103
  }
80
104
 
81
105
  const watched = new Map<string, WatchedContact>();
82
106
 
107
+ const newWatchers = (watcher: Watcher): Watchers<Watcher> => {
108
+ const watchers = new Watchers<Watcher>('contact watcher');
109
+ watchers.add(watcher);
110
+ return watchers;
111
+ };
112
+
83
113
  /**
84
114
  * Serializes an engine field value from an event the way the read API
85
115
  * would - both read the same engine value dict, keyed by the field's type
@@ -107,19 +137,28 @@ const serializeFieldValue = (key: string, value: any): string | undefined => {
107
137
  const appliers: {
108
138
  [type: string]: (contact: Contact, event: any) => boolean | void;
109
139
  } = {
110
- [Events.CONTACT_NAME_CHANGED]: (contact, event) => {
140
+ [Events.CONTACT_NAME_CHANGED]: (contact, event: ContactNameChangedEvent) => {
111
141
  contact.name = event.name;
112
142
  },
113
- [Events.CONTACT_LANGUAGE_CHANGED]: (contact, event) => {
143
+ [Events.CONTACT_LANGUAGE_CHANGED]: (
144
+ contact,
145
+ event: ContactLanguageChangedEvent
146
+ ) => {
114
147
  contact.language = event.language;
115
148
  },
116
- [Events.CONTACT_STATUS_CHANGED]: (contact, event) => {
149
+ [Events.CONTACT_STATUS_CHANGED]: (
150
+ contact,
151
+ event: ContactStatusChangedEvent
152
+ ) => {
117
153
  contact.status = event.status;
118
154
  },
119
- [Events.CONTACT_FLOW_CHANGED]: (contact, event) => {
155
+ [Events.CONTACT_FLOW_CHANGED]: (contact, event: ContactFlowChangedEvent) => {
120
156
  contact.flow = event.flow || null;
121
157
  },
122
- [Events.CONTACT_LAST_SEEN_CHANGED]: (contact, event) => {
158
+ [Events.CONTACT_LAST_SEEN_CHANGED]: (
159
+ contact,
160
+ event: ContactLastSeenChangedEvent
161
+ ) => {
123
162
  // last seen only ever moves forward - ignore out of order deliveries
124
163
  if (
125
164
  !contact.last_seen_on ||
@@ -128,7 +167,10 @@ const appliers: {
128
167
  contact.last_seen_on = event.last_seen_on;
129
168
  }
130
169
  },
131
- [Events.CONTACT_FIELD_CHANGED]: (contact, event) => {
170
+ [Events.CONTACT_FIELD_CHANGED]: (
171
+ contact,
172
+ event: ContactFieldChangedEvent
173
+ ) => {
132
174
  const key = event.field?.key;
133
175
  if (!key) {
134
176
  return;
@@ -141,19 +183,21 @@ const appliers: {
141
183
  // replace rather than mutate so earlier deliveries keep their values
142
184
  contact.fields = { ...contact.fields, [key]: value };
143
185
  },
144
- [Events.CONTACT_GROUPS_CHANGED]: (contact, event) => {
186
+ [Events.CONTACT_GROUPS_CHANGED]: (
187
+ contact,
188
+ event: ContactGroupsChangedEvent
189
+ ) => {
145
190
  const added = event.groups_added || [];
146
191
  const removed = new Set(
147
- (event.groups_removed || []).map((group: any) => group.uuid)
192
+ (event.groups_removed || []).map((group) => group.uuid)
148
193
  );
149
194
  contact.groups = [
150
195
  ...(contact.groups || []).filter(
151
196
  (group) =>
152
- !removed.has(group.uuid) &&
153
- !added.some((a: any) => a.uuid === group.uuid)
197
+ !removed.has(group.uuid) && !added.some((a) => a.uuid === group.uuid)
154
198
  ),
155
199
  // group references can arrive without a name, but consumers sort on it
156
- ...added.map((group: any) => ({
200
+ ...added.map((group) => ({
157
201
  uuid: group.uuid,
158
202
  name: group.name || ''
159
203
  }))
@@ -172,14 +216,31 @@ const matches = (watcher: Watcher, type: string): boolean => {
172
216
  return watcher.types === '*' || watcher.types.includes(type);
173
217
  };
174
218
 
175
- // watchers are page components we don't control - one of them throwing can't
176
- // be allowed to cost the others their delivery
177
- const deliver = (watcher: Watcher, event: any, contact: Contact) => {
178
- try {
179
- watcher.onEvent(event, contact);
180
- } catch (error) {
181
- console.error('contact watcher failed', error);
219
+ /**
220
+ * Whether anyone is watching this contact's *state*. Wildcard watchers are
221
+ * stream consumers - they take the events and keep their own view of the
222
+ * contact - so an entry with only those has no contact to hold current and
223
+ * nothing to fetch.
224
+ */
225
+ const needsContact = (entry: WatchedContact): boolean => {
226
+ return entry.watchers.some((watcher) => watcher.types !== '*');
227
+ };
228
+
229
+ /**
230
+ * Applies a live contact event to a contact, the same way the registry
231
+ * applies it to the one it holds. Exposed for stream consumers that keep
232
+ * their own copy, so how an event changes a contact is defined once.
233
+ * Returns false when the event couldn't be applied.
234
+ */
235
+ export const applyContactEvent = (
236
+ contact: Contact,
237
+ event: ContactHistoryEvent
238
+ ): boolean | void => {
239
+ const apply = appliers[event.type];
240
+ if (!contact || !apply) {
241
+ return;
182
242
  }
243
+ return apply(contact, event);
183
244
  };
184
245
 
185
246
  /**
@@ -187,11 +248,10 @@ const deliver = (watcher: Watcher, event: any, contact: Contact) => {
187
248
  * delivery.
188
249
  */
189
250
  const primeAll = (entry: WatchedContact) => {
190
- for (const watcher of [...entry.watchers]) {
191
- if (watcher.types !== '*') {
192
- deliver(watcher, null, entry.contact);
193
- }
194
- }
251
+ entry.watchers.each(
252
+ (watcher) => watcher.onEvent(null, entry.contact),
253
+ (watcher) => watcher.types !== '*'
254
+ );
195
255
  };
196
256
 
197
257
  const clearRefetch = (entry: WatchedContact) => {
@@ -286,7 +346,11 @@ const scheduleRefetch = (uuid: string, entry: WatchedContact) => {
286
346
  }, REFETCH_DEBOUNCE);
287
347
  };
288
348
 
289
- const handleEvent = (uuid: string, entry: WatchedContact, event: any) => {
349
+ const handleEvent = (
350
+ uuid: string,
351
+ entry: WatchedContact,
352
+ event: ContactHistoryEvent
353
+ ) => {
290
354
  const apply = appliers[event.type];
291
355
  const applied = entry.contact && apply ? apply(entry.contact, event) : null;
292
356
 
@@ -294,34 +358,58 @@ const handleEvent = (uuid: string, entry: WatchedContact, event: any) => {
294
358
  // was no snapshot to patch, a fetch that predates the event is still in
295
359
  // flight, or the applier couldn't resolve what it needed. Scheduling ahead
296
360
  // of the fan-out keeps a throwing watcher from costing us the refetch, and
297
- // the debounce collapses a burst into a single fetch
298
- const stale =
299
- !!apply && (!entry.contact || entry.fetching > 0 || applied === false);
300
- if (stale || refetchTypes.has(event.type)) {
301
- scheduleRefetch(uuid, entry);
361
+ // the debounce collapses a burst into a single fetch. Entries nobody needs
362
+ // a contact from have nothing to keep current
363
+ if (needsContact(entry)) {
364
+ const stale =
365
+ !!apply && (!entry.contact || entry.fetching > 0 || applied === false);
366
+ if (stale || refetchTypes.has(event.type)) {
367
+ scheduleRefetch(uuid, entry);
368
+ }
302
369
  }
303
370
 
304
- for (const watcher of [...entry.watchers]) {
305
- if (matches(watcher, event.type)) {
306
- deliver(watcher, event, entry.contact);
307
- }
371
+ entry.watchers.each(
372
+ (watcher) => watcher.onEvent(event, entry.contact),
373
+ (watcher) => matches(watcher, event.type)
374
+ );
375
+ };
376
+
377
+ /**
378
+ * A (re)subscribe on the contact's channel. Watchers that render contact
379
+ * state get a fresh fetch; every watcher that asked for it gets told, so
380
+ * stream consumers can fetch whatever they missed while we were away.
381
+ */
382
+ const handleSubscribed = (uuid: string, entry: WatchedContact) => {
383
+ entry.subscribed = true;
384
+ if (needsContact(entry)) {
385
+ fetchContact(uuid, entry);
308
386
  }
387
+ entry.watchers.each(
388
+ (watcher) => watcher.onSubscribed(),
389
+ (watcher) => !!watcher.onSubscribed
390
+ );
309
391
  };
310
392
 
311
393
  export const watchContact = (
312
394
  uuid: string,
313
395
  types: string[] | '*',
314
- onEvent: ContactEventHandler
396
+ onEvent: ContactEventHandler,
397
+ onSubscribed?: () => void
315
398
  ): RealtimeSubscription => {
316
- let entry = watched.get(uuid);
399
+ const entry = watched.get(uuid);
400
+ const watcher: Watcher = { types, onEvent, onSubscribed };
401
+
317
402
  if (!entry) {
318
403
  const newEntry: WatchedContact = {
319
- watchers: [],
404
+ // registered before we subscribe so the first (re)subscribe already
405
+ // knows whether anyone needs a contact held for them
406
+ watchers: newWatchers(watcher),
320
407
  sub: null,
321
408
  contact: null,
322
409
  fetchSeq: 0,
323
410
  fetching: 0,
324
- refetchTimer: null
411
+ refetchTimer: null,
412
+ subscribed: false
325
413
  };
326
414
  watched.set(uuid, newEntry);
327
415
  newEntry.sub = subscribeToContactHistory(
@@ -330,39 +418,54 @@ export const watchContact = (
330
418
  (event) => handleEvent(uuid, newEntry, event),
331
419
  // fires on every (re)subscribe incl. after reconnects - (re)fetch so
332
420
  // watchers see anything that changed while we weren't listening
333
- () => fetchContact(uuid, newEntry)
421
+ () => handleSubscribed(uuid, newEntry)
334
422
  );
335
- entry = newEntry;
423
+ return unwatch(uuid, newEntry, watcher);
336
424
  }
337
425
 
338
- const watcher: Watcher = { types, onEvent };
339
- entry.watchers.push(watcher);
426
+ entry.watchers.add(watcher);
427
+
428
+ // a watcher that renders contact state joining an entry that was holding
429
+ // none (only stream consumers so far) needs one fetched for it. On a
430
+ // channel that isn't live yet the pending (re)subscribe covers it
431
+ if (types !== '*' && entry.subscribed && !entry.contact && !entry.fetching) {
432
+ fetchContact(uuid, entry);
433
+ }
340
434
 
341
435
  // late joiners on an already-fetched contact get their initial delivery
342
436
  // without waiting for another fetch - async so it mirrors the fetch path
343
437
  if (entry.contact && types !== '*') {
344
438
  const contact = entry.contact;
345
- Promise.resolve().then(() => {
346
- if (entry.watchers.includes(watcher)) {
347
- deliver(watcher, null, contact);
348
- }
349
- });
439
+ entry.watchers.prime(watcher, () => watcher.onEvent(null, contact));
350
440
  }
351
441
 
352
- return {
353
- unsubscribe: () => {
354
- const index = entry.watchers.indexOf(watcher);
355
- if (index < 0) {
356
- return;
357
- }
358
- entry.watchers.splice(index, 1);
359
- if (entry.watchers.length === 0) {
360
- dropEntry(uuid, entry);
361
- }
362
- }
363
- };
442
+ // and joiners on an already-live channel get their initial catch-up call,
443
+ // which they'd otherwise wait for a reconnect to see
444
+ if (entry.subscribed && onSubscribed) {
445
+ entry.watchers.prime(watcher, () => watcher.onSubscribed());
446
+ }
447
+
448
+ return unwatch(uuid, entry, watcher);
364
449
  };
365
450
 
451
+ const unwatch = (
452
+ uuid: string,
453
+ entry: WatchedContact,
454
+ watcher: Watcher
455
+ ): RealtimeSubscription => ({
456
+ unsubscribe: () => {
457
+ if (entry.watchers.remove(watcher) && entry.watchers.size === 0) {
458
+ dropEntry(uuid, entry);
459
+ } else if (!needsContact(entry)) {
460
+ // only stream consumers left, and nothing keeps their contact current -
461
+ // holding onto this one would leave it drifting until a state watcher
462
+ // joined later and got primed with the drift
463
+ entry.contact = null;
464
+ clearRefetch(entry);
465
+ }
466
+ }
467
+ });
468
+
366
469
  /**
367
470
  * Pushes a fresh copy of a contact into the registry, priming its watchers.
368
471
  * Components that write contact changes call this with the server's response
@@ -409,7 +512,7 @@ const dropEntry = (uuid: string, entry: WatchedContact) => {
409
512
  // for tests - real pages just unwatch
410
513
  export const resetContactWatches = () => {
411
514
  for (const [uuid, entry] of [...watched.entries()]) {
412
- entry.watchers.length = 0;
515
+ entry.watchers.clear();
413
516
  dropEntry(uuid, entry);
414
517
  }
415
518
  };
@@ -1,4 +1,6 @@
1
- import { Notification } from '../interfaces';
1
+ import { Notification, ObjectReference, User } from '../interfaces';
2
+ import { Events } from '../events/eventRenderers';
3
+ import type { StoreAsset } from '../store/Store';
2
4
  import {
3
5
  PublicationHandler,
4
6
  SocketSubscription,
@@ -9,18 +11,137 @@ import {
9
11
  * Typed access to our realtime topics. SocketService owns the shared
10
12
  * connection and per-channel fan-out; this module owns how topics map to
11
13
  * channel names, including the page identity (org and user uuids) needed to
12
- * address user-scoped channels.
14
+ * address user-scoped channels, and what each topic publishes.
13
15
  *
14
16
  * The identity arrives via temba-store (hydrated from the page template), so
15
17
  * user-scoped subscriptions requested before the store mounts are queued and
16
18
  * activate when the context is set. On pages with no authenticated context
17
19
  * they simply never activate.
20
+ *
21
+ * Payload types below describe the wire, which is raw JSON - timestamps are
22
+ * strings here even where the rendered equivalents in events.ts carry Dates.
18
23
  */
19
24
 
20
25
  export interface RealtimeSubscription {
21
26
  unsubscribe(): void;
22
27
  }
23
28
 
29
+ /** Anything published on any of our topics. */
30
+ export interface RealtimeEvent {
31
+ type: string;
32
+ }
33
+
34
+ /**
35
+ * org:<org-uuid> - workspace-wide state every component on the page shares.
36
+ * An asset changed somewhere, so anything displaying it can update.
37
+ */
38
+ export interface AssetChangedEvent extends RealtimeEvent {
39
+ type: 'asset_changed';
40
+ asset: StoreAsset;
41
+ }
42
+
43
+ export type OrganizationEvent = AssetChangedEvent;
44
+
45
+ /**
46
+ * flow:<flow-uuid> - published once per committed sprint batch while a flow
47
+ * is running. It carries no counts, it just says they moved: the editor reads
48
+ * the current numbers over http when it sees one.
49
+ */
50
+ export interface FlowActivityEvent extends RealtimeEvent {
51
+ type: 'activity';
52
+ }
53
+
54
+ export type FlowEvent = FlowActivityEvent;
55
+
56
+ /**
57
+ * history:<contact-uuid> and history:<contact-uuid>:<ticket-uuid> - the
58
+ * engine's contact events (the payloads in events.ts, before their dates are
59
+ * parsed) plus the ephemeral ones below, which are never persisted.
60
+ */
61
+ export interface ContactHistoryEvent extends RealtimeEvent {
62
+ uuid?: string;
63
+ created_on?: string;
64
+ _user?: User;
65
+ // the rest of the payload varies by type. The contact-state events - the
66
+ // ones we read fields off rather than hand straight to the renderers - are
67
+ // typed below; events.ts describes the rendered shape of the others.
68
+ // unknown rather than any, so reading a field nobody declared is a type
69
+ // error at the point of use instead of silently spreading
70
+ [key: string]: unknown;
71
+ }
72
+
73
+ /**
74
+ * The events that change what a contact *is*, as opposed to recording
75
+ * something that happened to them. These are the ones ContactWatch applies to
76
+ * the contact it holds, so their payloads are what a server-side rename would
77
+ * break.
78
+ */
79
+ export interface ContactNameChangedEvent extends ContactHistoryEvent {
80
+ type: Events.CONTACT_NAME_CHANGED;
81
+ name: string;
82
+ }
83
+
84
+ export interface ContactLanguageChangedEvent extends ContactHistoryEvent {
85
+ type: Events.CONTACT_LANGUAGE_CHANGED;
86
+ language: string;
87
+ }
88
+
89
+ export interface ContactStatusChangedEvent extends ContactHistoryEvent {
90
+ type: Events.CONTACT_STATUS_CHANGED;
91
+ status: string;
92
+ }
93
+
94
+ export interface ContactFlowChangedEvent extends ContactHistoryEvent {
95
+ type: Events.CONTACT_FLOW_CHANGED;
96
+ flow: ObjectReference | null;
97
+ }
98
+
99
+ export interface ContactLastSeenChangedEvent extends ContactHistoryEvent {
100
+ type: Events.CONTACT_LAST_SEEN_CHANGED;
101
+ last_seen_on: string;
102
+ }
103
+
104
+ export interface ContactFieldChangedEvent extends ContactHistoryEvent {
105
+ type: Events.CONTACT_FIELD_CHANGED;
106
+ field: { key: string; name: string };
107
+ // engine field values always carry text; typed representations are present
108
+ // when the value parses as that type (see goflow's Value)
109
+ value: { text: string; datetime?: string; number?: string } | null;
110
+ }
111
+
112
+ export interface ContactGroupsChangedEvent extends ContactHistoryEvent {
113
+ type: Events.CONTACT_GROUPS_CHANGED;
114
+ groups_added?: ObjectReference[];
115
+ groups_removed?: ObjectReference[];
116
+ }
117
+
118
+ export interface ContactURNsChangedEvent extends ContactHistoryEvent {
119
+ type: Events.CONTACT_URNS_CHANGED;
120
+ urns: string[];
121
+ }
122
+
123
+ export type ContactStateEvent =
124
+ | ContactNameChangedEvent
125
+ | ContactLanguageChangedEvent
126
+ | ContactStatusChangedEvent
127
+ | ContactFlowChangedEvent
128
+ | ContactLastSeenChangedEvent
129
+ | ContactFieldChangedEvent
130
+ | ContactGroupsChangedEvent
131
+ | ContactURNsChangedEvent;
132
+
133
+ /**
134
+ * Published by agents as they compose, and echoed back to the publisher, so
135
+ * consumers filter out their own by _user.
136
+ */
137
+ export interface TypingEvent extends ContactHistoryEvent {
138
+ type: Events.TYPING_STARTED | Events.TYPING_STOPPED;
139
+ direction?: string;
140
+ // whatsapp expresses typing as an operation on the contact's last incoming
141
+ // message, so publications carry its external id when we have one
142
+ msg_external_id?: string;
143
+ }
144
+
24
145
  export interface RealtimeContext {
25
146
  org: string;
26
147
  user: string;
@@ -119,10 +240,14 @@ export const subscribeToNotifications = (
119
240
  * Workspace-wide state changes shared by every component on the page.
120
241
  */
121
242
  export const subscribeToOrganization = (
122
- onEvent: (event: any) => void,
243
+ onEvent: (event: OrganizationEvent) => void,
123
244
  onSubscribed?: () => void
124
245
  ): RealtimeSubscription => {
125
- return subscribeWhenReady((ctx) => `org:${ctx.org}`, onEvent, onSubscribed);
246
+ return subscribeWhenReady(
247
+ (ctx) => `org:${ctx.org}`,
248
+ (data) => onEvent(data as OrganizationEvent),
249
+ onSubscribed
250
+ );
126
251
  };
127
252
 
128
253
  /**
@@ -131,10 +256,14 @@ export const subscribeToOrganization = (
131
256
  */
132
257
  export const subscribeToFlow = (
133
258
  flow: string,
134
- onEvent: (event: any) => void,
259
+ onEvent: (event: FlowEvent) => void,
135
260
  onSubscribed?: () => void
136
261
  ): RealtimeSubscription => {
137
- return subscribeToSocket(`flow:${flow}`, onEvent, onSubscribed);
262
+ return subscribeToSocket(
263
+ `flow:${flow}`,
264
+ (data) => onEvent(data as FlowEvent),
265
+ onSubscribed
266
+ );
138
267
  };
139
268
 
140
269
  /**
@@ -144,12 +273,12 @@ export const subscribeToFlow = (
144
273
  export const subscribeToContactHistory = (
145
274
  contact: string,
146
275
  ticket: string | null,
147
- onEvent: (event: any) => void,
276
+ onEvent: (event: ContactHistoryEvent) => void,
148
277
  onSubscribed?: () => void
149
278
  ): RealtimeSubscription => {
150
279
  return subscribeToSocket(
151
280
  ticket ? `history:${contact}:${ticket}` : `history:${contact}`,
152
- onEvent,
281
+ (data) => onEvent(data as ContactHistoryEvent),
153
282
  onSubscribed
154
283
  );
155
284
  };