@knocklabs/client 0.10.13 → 0.11.0-rc.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 +11 -0
- package/dist/cjs/clients/in-app-messages/channel-client.js +2 -0
- package/dist/cjs/clients/in-app-messages/channel-client.js.map +1 -0
- package/dist/cjs/clients/in-app-messages/message-client.js +2 -0
- package/dist/cjs/clients/in-app-messages/message-client.js.map +1 -0
- package/dist/cjs/clients/in-app-messages/socket-manager.js +2 -0
- package/dist/cjs/clients/in-app-messages/socket-manager.js.map +1 -0
- package/dist/cjs/clients/in-app-messages/store.js +2 -0
- package/dist/cjs/clients/in-app-messages/store.js.map +1 -0
- package/dist/cjs/clients/users/index.js +1 -1
- package/dist/cjs/clients/users/index.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/esm/clients/in-app-messages/channel-client.mjs +80 -0
- package/dist/esm/clients/in-app-messages/channel-client.mjs.map +1 -0
- package/dist/esm/clients/in-app-messages/message-client.mjs +153 -0
- package/dist/esm/clients/in-app-messages/message-client.mjs.map +1 -0
- package/dist/esm/clients/in-app-messages/socket-manager.mjs +83 -0
- package/dist/esm/clients/in-app-messages/socket-manager.mjs.map +1 -0
- package/dist/esm/clients/in-app-messages/store.mjs +11 -0
- package/dist/esm/clients/in-app-messages/store.mjs.map +1 -0
- package/dist/esm/clients/users/index.mjs +28 -20
- package/dist/esm/clients/users/index.mjs.map +1 -1
- package/dist/esm/index.mjs +11 -7
- package/dist/esm/index.mjs.map +1 -1
- package/dist/types/clients/in-app-messages/channel-client.d.ts +23 -0
- package/dist/types/clients/in-app-messages/channel-client.d.ts.map +1 -0
- package/dist/types/clients/in-app-messages/index.d.ts +4 -0
- package/dist/types/clients/in-app-messages/index.d.ts.map +1 -0
- package/dist/types/clients/in-app-messages/message-client.d.ts +52 -0
- package/dist/types/clients/in-app-messages/message-client.d.ts.map +1 -0
- package/dist/types/clients/in-app-messages/socket-manager.d.ts +27 -0
- package/dist/types/clients/in-app-messages/socket-manager.d.ts.map +1 -0
- package/dist/types/clients/in-app-messages/store.d.ts +6 -0
- package/dist/types/clients/in-app-messages/store.d.ts.map +1 -0
- package/dist/types/clients/in-app-messages/types.d.ts +52 -0
- package/dist/types/clients/in-app-messages/types.d.ts.map +1 -0
- package/dist/types/clients/preferences/interfaces.d.ts +1 -1
- package/dist/types/clients/preferences/interfaces.d.ts.map +1 -1
- package/dist/types/clients/users/index.d.ts +3 -1
- package/dist/types/clients/users/index.d.ts.map +1 -1
- package/dist/types/clients/users/interfaces.d.ts +6 -0
- package/dist/types/clients/users/interfaces.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -9
- package/src/clients/in-app-messages/channel-client.ts +129 -0
- package/src/clients/in-app-messages/index.ts +3 -0
- package/src/clients/in-app-messages/message-client.ts +271 -0
- package/src/clients/in-app-messages/socket-manager.ts +187 -0
- package/src/clients/in-app-messages/store.ts +15 -0
- package/src/clients/in-app-messages/types.ts +79 -0
- package/src/clients/preferences/interfaces.ts +1 -1
- package/src/clients/users/index.ts +19 -1
- package/src/clients/users/interfaces.ts +8 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { GenericData } from "@knocklabs/types";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
|
|
4
|
+
import Knock from "../../knock";
|
|
5
|
+
import { NetworkStatus, isRequestInFlight } from "../../networkStatus";
|
|
6
|
+
import { MessageEngagementStatus } from "../messages/interfaces";
|
|
7
|
+
|
|
8
|
+
import { InAppMessagesChannelClient } from "./channel-client";
|
|
9
|
+
import { SocketEventPayload, SocketEventType } from "./socket-manager";
|
|
10
|
+
import {
|
|
11
|
+
InAppMessage,
|
|
12
|
+
InAppMessagesClientOptions,
|
|
13
|
+
InAppMessagesResponse,
|
|
14
|
+
InAppMessagesStoreState,
|
|
15
|
+
} from "./types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Manages realtime connection to in app messages service.
|
|
19
|
+
*/
|
|
20
|
+
export class InAppMessagesClient {
|
|
21
|
+
private knock: Knock;
|
|
22
|
+
|
|
23
|
+
public queryKey: string;
|
|
24
|
+
public referenceId: string;
|
|
25
|
+
public unsub?: () => void;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
readonly channelClient: InAppMessagesChannelClient,
|
|
29
|
+
readonly messageType: string,
|
|
30
|
+
readonly defaultOptions: InAppMessagesClientOptions = {},
|
|
31
|
+
) {
|
|
32
|
+
this.defaultOptions = {
|
|
33
|
+
...channelClient.defaultOptions,
|
|
34
|
+
...defaultOptions,
|
|
35
|
+
};
|
|
36
|
+
this.knock = channelClient.knock;
|
|
37
|
+
this.queryKey = this.buildQueryKey(this.defaultOptions);
|
|
38
|
+
this.referenceId = nanoid();
|
|
39
|
+
|
|
40
|
+
this.knock.log(`[IAM] Initialized a client for message ${messageType}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ----------------------------------------------
|
|
44
|
+
// Data fetching
|
|
45
|
+
// ----------------------------------------------
|
|
46
|
+
async fetch<
|
|
47
|
+
TContent extends GenericData = GenericData,
|
|
48
|
+
TData extends GenericData = GenericData,
|
|
49
|
+
>(): Promise<
|
|
50
|
+
| {
|
|
51
|
+
status: "ok";
|
|
52
|
+
data: InAppMessagesResponse<TContent, TData>;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
status: "error";
|
|
56
|
+
error: Error;
|
|
57
|
+
}
|
|
58
|
+
| undefined
|
|
59
|
+
> {
|
|
60
|
+
const params = this.defaultOptions;
|
|
61
|
+
|
|
62
|
+
this.queryKey = this.buildQueryKey(params);
|
|
63
|
+
|
|
64
|
+
const queryState = this.channelClient.store.state.queries[
|
|
65
|
+
this.queryKey
|
|
66
|
+
] ?? {
|
|
67
|
+
loading: false,
|
|
68
|
+
networkStatus: NetworkStatus.ready,
|
|
69
|
+
};
|
|
70
|
+
const networkStatus = queryState.networkStatus;
|
|
71
|
+
|
|
72
|
+
// If there's an existing request in flight, then do nothing
|
|
73
|
+
if (networkStatus && isRequestInFlight(networkStatus)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Set the loading type based on the request type it is
|
|
78
|
+
this.channelClient.setQueryStatus(this.queryKey, {
|
|
79
|
+
networkStatus: NetworkStatus.loading,
|
|
80
|
+
loading: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await this.knock.user.getInAppMessages<TContent, TData>({
|
|
85
|
+
channelId: this.channelClient.channelId,
|
|
86
|
+
messageType: this.messageType,
|
|
87
|
+
params,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.channelClient.setQueryResponse(this.queryKey, response);
|
|
91
|
+
|
|
92
|
+
return { data: response, status: "ok" };
|
|
93
|
+
} catch (error) {
|
|
94
|
+
this.channelClient.setQueryStatus(this.queryKey, {
|
|
95
|
+
networkStatus: NetworkStatus.error,
|
|
96
|
+
loading: false,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
status: "error",
|
|
101
|
+
error: error as Error,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getQueryInfoSelector<
|
|
107
|
+
TContent extends GenericData = GenericData,
|
|
108
|
+
TData extends GenericData = GenericData,
|
|
109
|
+
>(
|
|
110
|
+
state: InAppMessagesStoreState,
|
|
111
|
+
): {
|
|
112
|
+
messages: InAppMessage<TContent, TData>[];
|
|
113
|
+
loading: boolean;
|
|
114
|
+
networkStatus: NetworkStatus;
|
|
115
|
+
} {
|
|
116
|
+
const queryInfo = state.queries[this.queryKey];
|
|
117
|
+
const messageIds = queryInfo?.data?.messageIds ?? [];
|
|
118
|
+
|
|
119
|
+
const messages = messageIds.reduce<InAppMessage<TContent, TData>[]>(
|
|
120
|
+
(messages, messageId) => {
|
|
121
|
+
const message = state.messages[messageId];
|
|
122
|
+
if (message) {
|
|
123
|
+
messages.push(message as InAppMessage<TContent, TData>);
|
|
124
|
+
}
|
|
125
|
+
return messages;
|
|
126
|
+
},
|
|
127
|
+
[],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
messages,
|
|
132
|
+
networkStatus: queryInfo?.networkStatus ?? NetworkStatus.ready,
|
|
133
|
+
loading: queryInfo?.loading ?? false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ----------------------------------------------
|
|
138
|
+
// Message engagement
|
|
139
|
+
// ----------------------------------------------
|
|
140
|
+
async markAsSeen(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
141
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
142
|
+
|
|
143
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
144
|
+
seen_at: new Date().toISOString(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return this.makeStatusUpdate(itemOrItems, "seen");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async markAsUnseen(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
151
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
152
|
+
|
|
153
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
154
|
+
seen_at: null,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return this.makeStatusUpdate(itemOrItems, "unseen");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async markAsRead(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
161
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
162
|
+
|
|
163
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
164
|
+
read_at: new Date().toISOString(),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return this.makeStatusUpdate(itemOrItems, "read");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async markAsUnread(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
171
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
172
|
+
|
|
173
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
174
|
+
read_at: null,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return this.makeStatusUpdate(itemOrItems, "unread");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async markAsInteracted(
|
|
181
|
+
itemOrItems: InAppMessage | InAppMessage[],
|
|
182
|
+
metadata?: Record<string, string>,
|
|
183
|
+
) {
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
186
|
+
|
|
187
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
188
|
+
read_at: now,
|
|
189
|
+
interacted_at: now,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return this.makeStatusUpdate(itemOrItems, "interacted", metadata);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async markAsArchived(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
196
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
197
|
+
|
|
198
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
199
|
+
archived_at: new Date().toISOString(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return this.makeStatusUpdate(itemOrItems, "archived");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async markAsUnarchived(itemOrItems: InAppMessage | InAppMessage[]) {
|
|
206
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
207
|
+
|
|
208
|
+
this.channelClient.setMessageAttrs(itemIds, {
|
|
209
|
+
archived_at: null,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return this.makeStatusUpdate(itemOrItems, "unarchived");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async makeStatusUpdate(
|
|
216
|
+
itemOrItems: InAppMessage | InAppMessage[],
|
|
217
|
+
type: MessageEngagementStatus | "unread" | "unseen" | "unarchived",
|
|
218
|
+
metadata?: Record<string, string>,
|
|
219
|
+
) {
|
|
220
|
+
// Always treat items as a batch to use the corresponding batch endpoint
|
|
221
|
+
const itemIds = this.getItemIds(itemOrItems);
|
|
222
|
+
|
|
223
|
+
const result = await this.knock.messages.batchUpdateStatuses(
|
|
224
|
+
itemIds,
|
|
225
|
+
type,
|
|
226
|
+
{ metadata },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ----------------------------------------------
|
|
233
|
+
// Helpers
|
|
234
|
+
// ----------------------------------------------
|
|
235
|
+
private buildQueryKey(params: GenericData): string {
|
|
236
|
+
const baseKey = `/v1/users/${this.knock.userId}/in-app-messages/${this.channelClient.channelId}/${this.messageType}`;
|
|
237
|
+
const paramsString = new URLSearchParams(params).toString();
|
|
238
|
+
return paramsString ? `${baseKey}?${paramsString}` : baseKey;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
subscribe() {
|
|
242
|
+
this.unsub = this.channelClient.subscribe(this);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
unsubscribe() {
|
|
246
|
+
this.channelClient.unsubscribe(this);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// This is a callback function that will be invoked when a new socket event
|
|
250
|
+
// relevant for this message client is received (and if subscribed).
|
|
251
|
+
async handleSocketEvent(payload: SocketEventPayload) {
|
|
252
|
+
switch (payload.event) {
|
|
253
|
+
case SocketEventType.MessageCreated:
|
|
254
|
+
// TODO(KNO-7169): Explore using an in-app message in the socket event
|
|
255
|
+
// directly instead of re-fetching.
|
|
256
|
+
return await this.fetch();
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
throw new Error(`Unhandled socket event: ${payload.event}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
socketChannelTopic() {
|
|
264
|
+
return `in_app:${this.messageType}:${this.channelClient.channelId}:${this.knock.userId}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private getItemIds(itemOrItems: InAppMessage | InAppMessage[]): string[] {
|
|
268
|
+
const items = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
|
|
269
|
+
return items.map((item) => item.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Store } from "@tanstack/store";
|
|
2
|
+
import { Channel, Socket } from "phoenix";
|
|
3
|
+
|
|
4
|
+
import { InAppMessagesClient } from "./message-client";
|
|
5
|
+
import { InAppMessage, InAppMessagesClientOptions } from "./types";
|
|
6
|
+
|
|
7
|
+
export enum SocketEventType {
|
|
8
|
+
MessageCreated = "message.created",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SOCKET_EVENT_TYPES = [SocketEventType.MessageCreated];
|
|
12
|
+
|
|
13
|
+
type ClientQueryParams = InAppMessagesClientOptions;
|
|
14
|
+
|
|
15
|
+
// e.g. in_app:<message-type>:<channel_id>:<user_id>
|
|
16
|
+
type ChannelTopic = string;
|
|
17
|
+
|
|
18
|
+
// Unique reference id of an in-app message client
|
|
19
|
+
type ClientReferenceId = string;
|
|
20
|
+
|
|
21
|
+
type MessageCreatedEventPayload = {
|
|
22
|
+
event: SocketEventType.MessageCreated;
|
|
23
|
+
topic: string;
|
|
24
|
+
data: {
|
|
25
|
+
in_app_message: InAppMessage;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SocketEventPayload = MessageCreatedEventPayload;
|
|
30
|
+
|
|
31
|
+
// "attn" field contains a list of client reference ids that should be notified
|
|
32
|
+
// of a socket event.
|
|
33
|
+
type WithAttn<P> = P & { attn: Array<ClientReferenceId> };
|
|
34
|
+
|
|
35
|
+
type InAppMessageSocketInbox = Record<ClientReferenceId, SocketEventPayload>;
|
|
36
|
+
|
|
37
|
+
/*
|
|
38
|
+
* Manages socket subscriptions for in-app messages, allowing multiple in-app
|
|
39
|
+
* message clients or components to listen for real time updates from the socket
|
|
40
|
+
* API via a single socket connection. It's expected to be instantiated once
|
|
41
|
+
* per an in-app channel.
|
|
42
|
+
*/
|
|
43
|
+
export class InAppMessageSocketManager {
|
|
44
|
+
// Mapping of live channels by topic. Note, there can be one or more in-app
|
|
45
|
+
// message client(s) that can subscribe.
|
|
46
|
+
private channels: Record<ChannelTopic, Channel>;
|
|
47
|
+
|
|
48
|
+
// Mapping of query params for each in-app message client, partitioned by its
|
|
49
|
+
// reference id, and grouped by channel topic. It's a double nested object
|
|
50
|
+
// that looks like:
|
|
51
|
+
// {
|
|
52
|
+
// "in_app:card:...": {
|
|
53
|
+
// "ref-1": {
|
|
54
|
+
// "workflow_key": "foo",
|
|
55
|
+
// },
|
|
56
|
+
// "ref-2": {
|
|
57
|
+
// "workflow_key": "bar",
|
|
58
|
+
// },
|
|
59
|
+
// },
|
|
60
|
+
// "in_app:banner:...": {
|
|
61
|
+
// "ref-3": {
|
|
62
|
+
// "workflow_key": "baz",
|
|
63
|
+
// },
|
|
64
|
+
// }
|
|
65
|
+
// }
|
|
66
|
+
//
|
|
67
|
+
// Each time a new in-app message client joins a channel, we send all cumulated
|
|
68
|
+
// params such that the socket API can apply filtering rules and figure out
|
|
69
|
+
// which in-app message clients should be notified basd on reference ids in
|
|
70
|
+
// "attn" field of the event payload when sending out an event.
|
|
71
|
+
private params: Record<
|
|
72
|
+
ChannelTopic,
|
|
73
|
+
Record<ClientReferenceId, ClientQueryParams>
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
// A reactive store that captures a new socket event, that notifies any in-app
|
|
77
|
+
// message clients that have subscribed.
|
|
78
|
+
private inbox: Store<
|
|
79
|
+
InAppMessageSocketInbox,
|
|
80
|
+
(cb: InAppMessageSocketInbox) => InAppMessageSocketInbox
|
|
81
|
+
>;
|
|
82
|
+
|
|
83
|
+
constructor(readonly socket: Socket) {
|
|
84
|
+
this.channels = {};
|
|
85
|
+
this.params = {};
|
|
86
|
+
this.inbox = new Store<InAppMessageSocketInbox>({});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
join(iamClient: InAppMessagesClient) {
|
|
90
|
+
const topic = iamClient.socketChannelTopic();
|
|
91
|
+
const referenceId = iamClient.referenceId;
|
|
92
|
+
const params = iamClient.defaultOptions;
|
|
93
|
+
|
|
94
|
+
// Ensure a live socket connection if not yet connected.
|
|
95
|
+
if (!this.socket.isConnected()) {
|
|
96
|
+
this.socket.connect();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If a new in-app message client joins, or has updated query params then
|
|
100
|
+
// track the updated params and (re)join with the latest query params.
|
|
101
|
+
// Note, each time we send combined params of all in-app message clients that
|
|
102
|
+
// have subscribed for a given message type (and an in-app channel and a
|
|
103
|
+
// user), grouped by client's reference id.
|
|
104
|
+
if (!this.params[topic]) {
|
|
105
|
+
this.params[topic] = {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const maybeParams = this.params[topic][referenceId];
|
|
109
|
+
const hasNewOrUpdatedParams =
|
|
110
|
+
!maybeParams || JSON.stringify(maybeParams) !== JSON.stringify(params);
|
|
111
|
+
|
|
112
|
+
if (hasNewOrUpdatedParams) {
|
|
113
|
+
// Tracks all subscribed client's params by its reference id and by topic.
|
|
114
|
+
this.params[topic] = { ...this.params[topic], [referenceId]: params };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!this.channels[topic] || hasNewOrUpdatedParams) {
|
|
118
|
+
const newChannel = this.socket.channel(topic, this.params[topic]);
|
|
119
|
+
for (const eventType of SOCKET_EVENT_TYPES) {
|
|
120
|
+
newChannel.on(eventType, (payload) => this.setInbox(payload));
|
|
121
|
+
}
|
|
122
|
+
// Tracks live channels by channel topic.
|
|
123
|
+
this.channels[topic] = newChannel;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const channel = this.channels[topic];
|
|
127
|
+
|
|
128
|
+
// Join the channel if not already joined or joining or leaving.
|
|
129
|
+
if (["closed", "errored"].includes(channel.state)) {
|
|
130
|
+
channel.join();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Let the in-app message client subscribe to the "inbox", so it can be
|
|
134
|
+
// notified when there's a new socket event that is relevant for the client.
|
|
135
|
+
const unsub = this.inbox.subscribe(() => {
|
|
136
|
+
const payload = this.inbox.state[referenceId];
|
|
137
|
+
if (!payload) return;
|
|
138
|
+
|
|
139
|
+
iamClient.handleSocketEvent(payload);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return unsub;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
leave(iamClient: InAppMessagesClient) {
|
|
146
|
+
if (iamClient.unsub) {
|
|
147
|
+
iamClient.unsub();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const topic = iamClient.socketChannelTopic();
|
|
151
|
+
const referenceId = iamClient.referenceId;
|
|
152
|
+
|
|
153
|
+
const partitionedParams = { ...this.params };
|
|
154
|
+
const paramsForTopic = partitionedParams[topic] || {};
|
|
155
|
+
const paramsForReferenceClient = paramsForTopic[referenceId];
|
|
156
|
+
|
|
157
|
+
if (paramsForReferenceClient) {
|
|
158
|
+
delete paramsForTopic[referenceId];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const channels = { ...this.channels };
|
|
162
|
+
const channelForTopic = channels[topic];
|
|
163
|
+
if (channelForTopic && Object.keys(paramsForTopic).length === 0) {
|
|
164
|
+
for (const eventType of SOCKET_EVENT_TYPES) {
|
|
165
|
+
channelForTopic.off(eventType);
|
|
166
|
+
}
|
|
167
|
+
channelForTopic.leave();
|
|
168
|
+
delete channels[topic];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.params = partitionedParams;
|
|
172
|
+
this.channels = channels;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private setInbox(payload: WithAttn<SocketEventPayload>) {
|
|
176
|
+
const { attn, ...rest } = payload;
|
|
177
|
+
|
|
178
|
+
// Set the incoming socket event into the inbox, keyed by relevant client
|
|
179
|
+
// reference ids provided by the server (via attn field), so we can notify
|
|
180
|
+
// only the clients that need to be notified.
|
|
181
|
+
this.inbox.setState(() =>
|
|
182
|
+
attn.reduce((acc, referenceId) => {
|
|
183
|
+
return { ...acc, [referenceId]: rest };
|
|
184
|
+
}, {}),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Store } from "@tanstack/store";
|
|
2
|
+
|
|
3
|
+
import { InAppMessagesStoreState } from "./types";
|
|
4
|
+
|
|
5
|
+
export type InAppMessagesStore = Store<
|
|
6
|
+
InAppMessagesStoreState,
|
|
7
|
+
(cb: InAppMessagesStoreState) => InAppMessagesStoreState
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export function createStore() {
|
|
11
|
+
return new Store<InAppMessagesStoreState>({
|
|
12
|
+
messages: {},
|
|
13
|
+
queries: {},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GenericData, PageInfo, Tenant } from "@knocklabs/types";
|
|
2
|
+
|
|
3
|
+
import { NetworkStatus } from "../../networkStatus";
|
|
4
|
+
import { NotificationSource } from "../messages/interfaces";
|
|
5
|
+
|
|
6
|
+
export interface InAppMessage<
|
|
7
|
+
TContent extends GenericData = GenericData,
|
|
8
|
+
TData extends GenericData = GenericData,
|
|
9
|
+
TTenantProperties = GenericData,
|
|
10
|
+
> {
|
|
11
|
+
__cursor: string;
|
|
12
|
+
id: string;
|
|
13
|
+
message_type: string;
|
|
14
|
+
schema_variant: string;
|
|
15
|
+
schema_version: string;
|
|
16
|
+
content: TContent;
|
|
17
|
+
data: TData | null;
|
|
18
|
+
inserted_at: string;
|
|
19
|
+
updated_at: string;
|
|
20
|
+
seen_at: string | null;
|
|
21
|
+
read_at: string | null;
|
|
22
|
+
interacted_at: string | null;
|
|
23
|
+
archived_at: string | null;
|
|
24
|
+
link_clicked_at: string | null;
|
|
25
|
+
source: NotificationSource;
|
|
26
|
+
tenant: Tenant<TTenantProperties>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InAppMessagesResponse<
|
|
30
|
+
TContent extends GenericData = GenericData,
|
|
31
|
+
TData extends GenericData = GenericData,
|
|
32
|
+
> {
|
|
33
|
+
entries: InAppMessage<TContent, TData>[];
|
|
34
|
+
pageInfo: PageInfo;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface InAppMessagesQueryInfo {
|
|
38
|
+
networkStatus: NetworkStatus;
|
|
39
|
+
loading: boolean;
|
|
40
|
+
data?: {
|
|
41
|
+
messageIds: string[];
|
|
42
|
+
pageInfo: PageInfo;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface InAppMessagesStoreState {
|
|
47
|
+
messages: Record<string, InAppMessage>;
|
|
48
|
+
queries: Record<string, InAppMessagesQueryInfo>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type InAppMessageEngagementStatus =
|
|
52
|
+
| "read"
|
|
53
|
+
| "unread"
|
|
54
|
+
| "seen"
|
|
55
|
+
| "unseen"
|
|
56
|
+
| "link_clicked"
|
|
57
|
+
| "link_unclicked"
|
|
58
|
+
| "interacted"
|
|
59
|
+
| "uninteracted";
|
|
60
|
+
|
|
61
|
+
export interface InAppMessagesClientOptions {
|
|
62
|
+
order?: "asc" | "desc";
|
|
63
|
+
before?: string;
|
|
64
|
+
after?: string;
|
|
65
|
+
page_size?: number;
|
|
66
|
+
engagement_status?: InAppMessageEngagementStatus[];
|
|
67
|
+
// Optionally scope all requests to a particular tenant or tenants
|
|
68
|
+
tenant_id?: string | string[];
|
|
69
|
+
// Optionally scope to notifications from the given workflow or workflows
|
|
70
|
+
workflow_key?: string | string[];
|
|
71
|
+
// Optionally scope to notifications with any of the categories provided
|
|
72
|
+
workflow_categories?: string[];
|
|
73
|
+
// Optionally scope to a given archived status (defaults to `exclude`)
|
|
74
|
+
archived?: "include" | "exclude" | "only";
|
|
75
|
+
// Optionally scope all notifications that contain this argument as part of their trigger payload
|
|
76
|
+
// TODO(KNO-7140): This currently does not work because the API expects this
|
|
77
|
+
// to be a json string.
|
|
78
|
+
trigger_data?: GenericData;
|
|
79
|
+
}
|
|
@@ -3,6 +3,7 @@ import { GenericData } from "@knocklabs/types";
|
|
|
3
3
|
import { ApiResponse } from "../../api";
|
|
4
4
|
import { ChannelData, User } from "../../interfaces";
|
|
5
5
|
import Knock from "../../knock";
|
|
6
|
+
import { InAppMessagesResponse } from "../in-app-messages";
|
|
6
7
|
import {
|
|
7
8
|
GetPreferencesOptions,
|
|
8
9
|
PreferenceOptions,
|
|
@@ -10,7 +11,11 @@ import {
|
|
|
10
11
|
SetPreferencesProperties,
|
|
11
12
|
} from "../preferences/interfaces";
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
GetChannelDataInput,
|
|
16
|
+
GetInAppMessagesInput,
|
|
17
|
+
SetChannelDataInput,
|
|
18
|
+
} from "./interfaces";
|
|
14
19
|
|
|
15
20
|
const DEFAULT_PREFERENCE_SET_ID = "default";
|
|
16
21
|
|
|
@@ -100,6 +105,19 @@ class UserClient {
|
|
|
100
105
|
return this.handleResponse<ChannelData<T>>(result);
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
async getInAppMessages<
|
|
109
|
+
TContent extends GenericData = GenericData,
|
|
110
|
+
TData extends GenericData = GenericData,
|
|
111
|
+
>({ channelId, messageType, params }: GetInAppMessagesInput) {
|
|
112
|
+
const result = await this.instance.client().makeRequest({
|
|
113
|
+
method: "GET",
|
|
114
|
+
url: `/v1/users/${this.instance.userId}/in-app-messages/${channelId}/${messageType}`,
|
|
115
|
+
params,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return this.handleResponse<InAppMessagesResponse<TContent, TData>>(result);
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
private handleResponse<T>(response: ApiResponse) {
|
|
104
122
|
if (response.statusCode === "error") {
|
|
105
123
|
throw new Error(response.error || response.body);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { GenericData } from "@knocklabs/types";
|
|
2
2
|
|
|
3
|
+
import { InAppMessagesClientOptions } from "../in-app-messages/types";
|
|
4
|
+
|
|
3
5
|
export interface SetChannelDataInput {
|
|
4
6
|
channelId: string;
|
|
5
7
|
channelData: GenericData;
|
|
@@ -8,3 +10,9 @@ export interface SetChannelDataInput {
|
|
|
8
10
|
export interface GetChannelDataInput {
|
|
9
11
|
channelId: string;
|
|
10
12
|
}
|
|
13
|
+
|
|
14
|
+
export interface GetInAppMessagesInput {
|
|
15
|
+
channelId: string;
|
|
16
|
+
messageType: string;
|
|
17
|
+
params: InAppMessagesClientOptions;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./clients/slack/interfaces";
|
|
|
12
12
|
export * from "./clients/users";
|
|
13
13
|
export * from "./clients/users/interfaces";
|
|
14
14
|
export * from "./clients/messages";
|
|
15
|
+
export * from "./clients/in-app-messages";
|
|
15
16
|
export * from "./clients/messages/interfaces";
|
|
16
17
|
export * from "./networkStatus";
|
|
17
18
|
|