@repzo/widget-react-native 1.1.0 → 1.2.1
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 +64 -0
- package/README.md +92 -3
- package/lib/commonjs/api/client.js +35 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/i18n.js +14 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/index.js +2 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/locales.js +13 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/push/push-notifications.js +65 -8
- package/lib/commonjs/push/push-notifications.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +451 -7
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/types.js +15 -0
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +64 -2
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/Post.js +1241 -0
- package/lib/commonjs/ui/components/Post.js.map +1 -0
- package/lib/commonjs/ui/utils/html.js +1 -0
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/post-html.js +276 -0
- package/lib/commonjs/ui/utils/post-html.js.map +1 -0
- package/lib/commonjs/ui/utils/safe-url.js +16 -0
- package/lib/commonjs/ui/utils/safe-url.js.map +1 -1
- package/lib/module/api/client.js +35 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/i18n.js +14 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/locales.js +13 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/push/push-notifications.js +65 -8
- package/lib/module/push/push-notifications.js.map +1 -1
- package/lib/module/state/widget-store.js +450 -6
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/types.js +15 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +65 -3
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/Post.js +1236 -0
- package/lib/module/ui/components/Post.js.map +1 -0
- package/lib/module/ui/utils/html.js +1 -1
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/post-html.js +271 -0
- package/lib/module/ui/utils/post-html.js.map +1 -0
- package/lib/module/ui/utils/safe-url.js +15 -0
- package/lib/module/ui/utils/safe-url.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +21 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +13 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/push/push-notifications.d.ts +26 -2
- package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +84 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +77 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Post.d.ts +69 -0
- package/lib/typescript/commonjs/ui/components/Post.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts +1 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/post-html.d.ts +54 -0
- package/lib/typescript/commonjs/ui/utils/post-html.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/safe-url.d.ts +6 -0
- package/lib/typescript/commonjs/ui/utils/safe-url.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +21 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +13 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/push/push-notifications.d.ts +26 -2
- package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +84 -1
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +77 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Post.d.ts +69 -0
- package/lib/typescript/module/ui/components/Post.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/html.d.ts +1 -0
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/post-html.d.ts +54 -0
- package/lib/typescript/module/ui/utils/post-html.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/safe-url.d.ts +6 -0
- package/lib/typescript/module/ui/utils/safe-url.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/client.ts +32 -0
- package/src/i18n.ts +14 -0
- package/src/index.tsx +17 -0
- package/src/locales.ts +13 -0
- package/src/push/push-notifications.ts +74 -8
- package/src/state/widget-store.ts +429 -6
- package/src/types.ts +77 -0
- package/src/ui/WidgetProvider.tsx +78 -2
- package/src/ui/components/Post.tsx +943 -0
- package/src/ui/utils/html.ts +1 -1
- package/src/ui/utils/post-html.ts +256 -0
- package/src/ui/utils/safe-url.ts +15 -0
package/src/api/client.ts
CHANGED
|
@@ -16,8 +16,11 @@ import type {
|
|
|
16
16
|
InitResponse,
|
|
17
17
|
Message,
|
|
18
18
|
OutboundMessage,
|
|
19
|
+
PostInteractionKind,
|
|
20
|
+
PostReply,
|
|
19
21
|
RepzoWidgetErrorCode,
|
|
20
22
|
WidgetConfig,
|
|
23
|
+
WidgetPost,
|
|
21
24
|
WidgetUser,
|
|
22
25
|
} from '../types';
|
|
23
26
|
import { isHttpUrl, urlScheme } from '../ui/utils/safe-url';
|
|
@@ -194,6 +197,30 @@ export class WidgetApiClient {
|
|
|
194
197
|
return this.request(`/api/widget/outbound/${encodeURIComponent(this.channelAccountId)}`);
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/**
|
|
201
|
+
* The Send post this visitor should see now, if any.
|
|
202
|
+
* `recheckInMs`: ask again after this long — the shown one stops then.
|
|
203
|
+
*/
|
|
204
|
+
getPost(): Promise<{ post: WidgetPost | null; recheckInMs?: number | null }> {
|
|
205
|
+
return this.request('/api/widget/send/post?platform=mobile');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Reports what the visitor did with a post; `hidden` means load the next
|
|
210
|
+
* one. A reply comes back as saved (`reply`) — it's also delivered to the
|
|
211
|
+
* post's conversation in the inbox, where the team answers.
|
|
212
|
+
*/
|
|
213
|
+
sendPostInteraction(
|
|
214
|
+
campaignId: string,
|
|
215
|
+
kind: PostInteractionKind,
|
|
216
|
+
value?: string
|
|
217
|
+
): Promise<{ hidden: boolean; reply?: PostReply; conversationId?: string | null }> {
|
|
218
|
+
return this.request(`/api/widget/send/posts/${encodeURIComponent(campaignId)}/interactions`, {
|
|
219
|
+
method: 'POST',
|
|
220
|
+
body: value === undefined ? { kind } : { kind, value },
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
197
224
|
/**
|
|
198
225
|
* Does NOT store the returned token — the store persists it only once it
|
|
199
226
|
* knows the identity op that asked for it is still current.
|
|
@@ -340,6 +367,11 @@ export class WidgetApiClient {
|
|
|
340
367
|
return this.request('/api/widget/push-token', { method: 'POST', body: data });
|
|
341
368
|
}
|
|
342
369
|
|
|
370
|
+
/** A Send push (`document_type: 'send_push'`) was tapped — counts as an open in Workstation. */
|
|
371
|
+
reportPushOpened(deliveryId: string): Promise<void> {
|
|
372
|
+
return this.request('/api/widget/push-opened', { method: 'POST', body: { deliveryId } });
|
|
373
|
+
}
|
|
374
|
+
|
|
343
375
|
/** Logout passes the session's token explicitly and `skipReauth`. */
|
|
344
376
|
removePushToken(
|
|
345
377
|
token: string,
|
package/src/i18n.ts
CHANGED
|
@@ -157,6 +157,20 @@ export const DEFAULT_LABELS = {
|
|
|
157
157
|
notificationNewReply: 'New reply',
|
|
158
158
|
notificationAttachment: 'Sent an attachment',
|
|
159
159
|
notificationOpenHint: 'Opens the conversation',
|
|
160
|
+
// Send posts over the app.
|
|
161
|
+
postFrom: '{name} from {workspace}',
|
|
162
|
+
postNewMessage: 'New message',
|
|
163
|
+
postOpenHint: 'Opens the message',
|
|
164
|
+
postClose: 'Close',
|
|
165
|
+
postMinimize: 'Minimize',
|
|
166
|
+
postReplyPlaceholder: 'Write a reply…',
|
|
167
|
+
postSend: 'Send',
|
|
168
|
+
postSent: 'Sent',
|
|
169
|
+
postReplyFailed: "Couldn't send your reply. Try again.",
|
|
170
|
+
postReactions: 'Reactions',
|
|
171
|
+
postThanks: 'Thanks for your feedback!',
|
|
172
|
+
postConversation: 'Your conversation',
|
|
173
|
+
postContinueChat: 'Continue in Messenger',
|
|
160
174
|
// Rich blocks an agent can send.
|
|
161
175
|
blockService: 'Service',
|
|
162
176
|
blockProduct: 'Product',
|
package/src/index.tsx
CHANGED
|
@@ -33,6 +33,9 @@ export type {
|
|
|
33
33
|
InAppNotification,
|
|
34
34
|
Message,
|
|
35
35
|
OutboundMessage,
|
|
36
|
+
PostInteractionKind,
|
|
37
|
+
PostReply,
|
|
38
|
+
PostThreadMessage,
|
|
36
39
|
PreChatField,
|
|
37
40
|
RepzoPickedFile,
|
|
38
41
|
RepzoWidgetAttachmentOptions,
|
|
@@ -41,6 +44,7 @@ export type {
|
|
|
41
44
|
WidgetAttachment,
|
|
42
45
|
WidgetConfig,
|
|
43
46
|
WidgetCustomAttributes,
|
|
47
|
+
WidgetPost,
|
|
44
48
|
WidgetSpace,
|
|
45
49
|
WidgetSpaceType,
|
|
46
50
|
WidgetState,
|
|
@@ -152,6 +156,17 @@ export type RepzoWidgetType = {
|
|
|
152
156
|
* installed, `boot({ push: { enabled: true } })` handles everything).
|
|
153
157
|
*/
|
|
154
158
|
setPushToken(options: { token: string; platform?: 'ios' | 'android'; bundleId?: string }): Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hand over a tapped notification your app read itself (Firebase's
|
|
162
|
+
* `getInitialNotification` / `onNotificationOpenedApp`, or a notification
|
|
163
|
+
* you re-posted locally). Returns true when it's a Repzo Widget push — it
|
|
164
|
+
* opens (now, or once the session is ready) and your app should skip its
|
|
165
|
+
* own routing; false for your app's own pushes.
|
|
166
|
+
*
|
|
167
|
+
* @since 1.2.1
|
|
168
|
+
*/
|
|
169
|
+
handlePushNotification(message: { messageId?: string; data?: Record<string, unknown> }): boolean;
|
|
155
170
|
};
|
|
156
171
|
|
|
157
172
|
const RepzoWidget: RepzoWidgetType = {
|
|
@@ -198,6 +213,8 @@ const RepzoWidget: RepzoWidgetType = {
|
|
|
198
213
|
setSecureStorageAdapter: (adapter) => setSecureStorageAdapter(adapter),
|
|
199
214
|
|
|
200
215
|
setPushToken: (options) => controller.store.push.setManualToken(options),
|
|
216
|
+
|
|
217
|
+
handlePushNotification: (message) => controller.store.push.handleNotification(message),
|
|
201
218
|
};
|
|
202
219
|
|
|
203
220
|
export default RepzoWidget;
|
package/src/locales.ts
CHANGED
|
@@ -107,6 +107,19 @@ const ar: LabelMap = {
|
|
|
107
107
|
notificationNewReply: 'رد جديد',
|
|
108
108
|
notificationAttachment: 'أرسل مرفقًا',
|
|
109
109
|
notificationOpenHint: 'يفتح المحادثة',
|
|
110
|
+
postFrom: '{name} من {workspace}',
|
|
111
|
+
postNewMessage: 'رسالة جديدة',
|
|
112
|
+
postOpenHint: 'يفتح الرسالة',
|
|
113
|
+
postClose: 'إغلاق',
|
|
114
|
+
postMinimize: 'تصغير',
|
|
115
|
+
postReplyPlaceholder: 'اكتب ردًا…',
|
|
116
|
+
postSend: 'إرسال',
|
|
117
|
+
postSent: 'تم الإرسال',
|
|
118
|
+
postReplyFailed: 'تعذّر إرسال ردك. حاول مرة أخرى.',
|
|
119
|
+
postReactions: 'التفاعلات',
|
|
120
|
+
postThanks: 'شكراً على ملاحظاتك!',
|
|
121
|
+
postConversation: 'محادثتك',
|
|
122
|
+
postContinueChat: 'تابع في المحادثة',
|
|
110
123
|
blockService: 'خدمة',
|
|
111
124
|
blockProduct: 'منتج',
|
|
112
125
|
blockChooseTime: 'اختر وقتًا',
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { Platform } from 'react-native';
|
|
17
17
|
import type { WidgetApiClient } from '../api/client';
|
|
18
|
+
import { openExternalUrl } from '../ui/utils/safe-url';
|
|
18
19
|
|
|
19
20
|
export interface WidgetPushOptions {
|
|
20
21
|
/** Register this device for FCM pushes after every successful session init. */
|
|
@@ -90,6 +91,18 @@ function resolveMessaging(): MessagingHandle | null {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
const WIDGET_DOCUMENT_TYPES = new Set(['widget_message', 'send_post', 'send_push']);
|
|
95
|
+
|
|
96
|
+
/** A push sent by Workstation for the widget (vs. one of the host app's own). */
|
|
97
|
+
function isWidgetPush(data: any): data is Record<string, unknown> & { document_id: string } {
|
|
98
|
+
return (
|
|
99
|
+
!!data &&
|
|
100
|
+
WIDGET_DOCUMENT_TYPES.has(data.document_type) &&
|
|
101
|
+
typeof data.document_id === 'string' &&
|
|
102
|
+
!!data.document_id
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
export class PushManager {
|
|
94
107
|
private options: WidgetPushOptions = {};
|
|
95
108
|
|
|
@@ -107,9 +120,16 @@ export class PushManager {
|
|
|
107
120
|
/** Registrations still on the wire; unregister() waits for them. */
|
|
108
121
|
private inflight = new Set<Promise<void>>();
|
|
109
122
|
|
|
123
|
+
/** Taps the host handed over before the session was ready; start() opens them. */
|
|
124
|
+
private pendingTaps: any[] = [];
|
|
125
|
+
|
|
126
|
+
/** The host and our own listener can both see one tap — open it once. */
|
|
127
|
+
private handledMessageIds = new Set<string>();
|
|
128
|
+
|
|
110
129
|
constructor(
|
|
111
130
|
private client: WidgetApiClient,
|
|
112
|
-
private openConversation: (conversationId: string) => void
|
|
131
|
+
private openConversation: (conversationId: string) => void,
|
|
132
|
+
private openPost: (campaignId: string) => void = () => {}
|
|
113
133
|
) {}
|
|
114
134
|
|
|
115
135
|
/** Remember the boot options; actual work happens in start() post-init. */
|
|
@@ -124,6 +144,9 @@ export class PushManager {
|
|
|
124
144
|
*/
|
|
125
145
|
async start(): Promise<void> {
|
|
126
146
|
this.active = true;
|
|
147
|
+
const pending = this.pendingTaps;
|
|
148
|
+
this.pendingTaps = [];
|
|
149
|
+
pending.forEach((message) => this.handleTap(message));
|
|
127
150
|
if (this.manualToken) {
|
|
128
151
|
await this.register(this.manualToken);
|
|
129
152
|
}
|
|
@@ -192,6 +215,8 @@ export class PushManager {
|
|
|
192
215
|
*/
|
|
193
216
|
async unregister(authToken: string | null): Promise<void> {
|
|
194
217
|
this.active = false;
|
|
218
|
+
// A tap held for this session must not open in the next user's.
|
|
219
|
+
this.pendingTaps = [];
|
|
195
220
|
await Promise.all([...this.inflight]);
|
|
196
221
|
const token = this.lastToken;
|
|
197
222
|
this.lastToken = null;
|
|
@@ -227,27 +252,68 @@ export class PushManager {
|
|
|
227
252
|
}
|
|
228
253
|
|
|
229
254
|
private wireTapHandlers(messaging: MessagingHandle) {
|
|
255
|
+
// These listeners outlive logout, so taps go through the same session gate
|
|
256
|
+
// as host-forwarded ones: held until start(), and only marked handled then.
|
|
230
257
|
// Background → tapped: the app was alive, just not in front.
|
|
231
258
|
messaging.onNotificationOpenedApp((remoteMessage: any) => {
|
|
232
|
-
this.
|
|
259
|
+
this.handleNotification(remoteMessage);
|
|
233
260
|
});
|
|
234
261
|
|
|
235
262
|
// Killed → tapped: the click that launched the app.
|
|
236
263
|
void messaging
|
|
237
264
|
.getInitialNotification()
|
|
238
265
|
.then((remoteMessage: any) => {
|
|
239
|
-
if (remoteMessage) this.
|
|
266
|
+
if (remoteMessage) this.handleNotification(remoteMessage);
|
|
240
267
|
})
|
|
241
268
|
.catch(() => {});
|
|
242
269
|
}
|
|
243
270
|
|
|
244
|
-
/**
|
|
271
|
+
/**
|
|
272
|
+
* A tap the host app read itself (`RepzoWidget.handlePushNotification`).
|
|
273
|
+
* Firebase hands out the notification that launched the app only once, so
|
|
274
|
+
* when the host reads it first our own listener gets nothing; the same goes
|
|
275
|
+
* for notifications the host re-posts locally while in the foreground.
|
|
276
|
+
* True when it's a widget push (so the host skips its own routing); it opens
|
|
277
|
+
* now, or once the session is ready.
|
|
278
|
+
*/
|
|
279
|
+
handleNotification(message: any): boolean {
|
|
280
|
+
if (!isWidgetPush(message?.data)) return false;
|
|
281
|
+
if (this.active) this.handleTap(message);
|
|
282
|
+
else this.pendingTaps.push(message);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Only widget pushes are ours — host app notifications pass through. A team
|
|
288
|
+
* reply opens its conversation; a Send post opens the post; a Send mobile
|
|
289
|
+
* push reports the open and follows its link, if it has one.
|
|
290
|
+
*/
|
|
245
291
|
private handleTap(remoteMessage: any) {
|
|
246
292
|
const data = remoteMessage?.data;
|
|
247
|
-
if (data
|
|
248
|
-
const
|
|
249
|
-
if (typeof
|
|
250
|
-
this.
|
|
293
|
+
if (!isWidgetPush(data)) return;
|
|
294
|
+
const messageId = remoteMessage?.messageId;
|
|
295
|
+
if (typeof messageId === 'string' && messageId) {
|
|
296
|
+
if (this.handledMessageIds.has(messageId)) return;
|
|
297
|
+
this.handledMessageIds.add(messageId);
|
|
298
|
+
}
|
|
299
|
+
const documentId = data.document_id;
|
|
300
|
+
if (data?.document_type === 'widget_message') this.openConversation(documentId);
|
|
301
|
+
else if (data?.document_type === 'send_post') this.openPost(documentId);
|
|
302
|
+
else if (data?.document_type === 'send_push') this.openSendPush(data);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The tap already opened the app. A link goes through the same gate as links
|
|
307
|
+
* in messages (`onOpenUrl` first, then web/mail/phone links), so the host can
|
|
308
|
+
* route `myapp://…` itself.
|
|
309
|
+
*/
|
|
310
|
+
private openSendPush(data: Record<string, unknown>) {
|
|
311
|
+
const deliveryId = data.delivery_id;
|
|
312
|
+
if (typeof deliveryId === 'string' && deliveryId) {
|
|
313
|
+
this.client.reportPushOpened(deliveryId).catch(() => {
|
|
314
|
+
// Only the open count misses it; the visitor sees nothing.
|
|
315
|
+
});
|
|
251
316
|
}
|
|
317
|
+
if (typeof data.url === 'string' && data.url) void openExternalUrl(data.url);
|
|
252
318
|
}
|
|
253
319
|
}
|