@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.
Files changed (107) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +92 -3
  3. package/lib/commonjs/api/client.js +35 -0
  4. package/lib/commonjs/api/client.js.map +1 -1
  5. package/lib/commonjs/i18n.js +14 -0
  6. package/lib/commonjs/i18n.js.map +1 -1
  7. package/lib/commonjs/index.js +2 -1
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/locales.js +13 -0
  10. package/lib/commonjs/locales.js.map +1 -1
  11. package/lib/commonjs/push/push-notifications.js +65 -8
  12. package/lib/commonjs/push/push-notifications.js.map +1 -1
  13. package/lib/commonjs/state/widget-store.js +451 -7
  14. package/lib/commonjs/state/widget-store.js.map +1 -1
  15. package/lib/commonjs/types.js +15 -0
  16. package/lib/commonjs/types.js.map +1 -1
  17. package/lib/commonjs/ui/WidgetProvider.js +64 -2
  18. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  19. package/lib/commonjs/ui/components/Post.js +1241 -0
  20. package/lib/commonjs/ui/components/Post.js.map +1 -0
  21. package/lib/commonjs/ui/utils/html.js +1 -0
  22. package/lib/commonjs/ui/utils/html.js.map +1 -1
  23. package/lib/commonjs/ui/utils/post-html.js +276 -0
  24. package/lib/commonjs/ui/utils/post-html.js.map +1 -0
  25. package/lib/commonjs/ui/utils/safe-url.js +16 -0
  26. package/lib/commonjs/ui/utils/safe-url.js.map +1 -1
  27. package/lib/module/api/client.js +35 -0
  28. package/lib/module/api/client.js.map +1 -1
  29. package/lib/module/i18n.js +14 -0
  30. package/lib/module/i18n.js.map +1 -1
  31. package/lib/module/index.js +2 -1
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/locales.js +13 -0
  34. package/lib/module/locales.js.map +1 -1
  35. package/lib/module/push/push-notifications.js +65 -8
  36. package/lib/module/push/push-notifications.js.map +1 -1
  37. package/lib/module/state/widget-store.js +450 -6
  38. package/lib/module/state/widget-store.js.map +1 -1
  39. package/lib/module/types.js +15 -0
  40. package/lib/module/types.js.map +1 -1
  41. package/lib/module/ui/WidgetProvider.js +65 -3
  42. package/lib/module/ui/WidgetProvider.js.map +1 -1
  43. package/lib/module/ui/components/Post.js +1236 -0
  44. package/lib/module/ui/components/Post.js.map +1 -0
  45. package/lib/module/ui/utils/html.js +1 -1
  46. package/lib/module/ui/utils/html.js.map +1 -1
  47. package/lib/module/ui/utils/post-html.js +271 -0
  48. package/lib/module/ui/utils/post-html.js.map +1 -0
  49. package/lib/module/ui/utils/safe-url.js +15 -0
  50. package/lib/module/ui/utils/safe-url.js.map +1 -1
  51. package/lib/typescript/commonjs/api/client.d.ts +21 -1
  52. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/i18n.d.ts +13 -0
  54. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/index.d.ts +14 -1
  56. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/push/push-notifications.d.ts +26 -2
  59. package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/state/widget-store.d.ts +84 -1
  61. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/types.d.ts +77 -0
  63. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/Post.d.ts +69 -0
  66. package/lib/typescript/commonjs/ui/components/Post.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/ui/utils/html.d.ts +1 -0
  68. package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/utils/post-html.d.ts +54 -0
  70. package/lib/typescript/commonjs/ui/utils/post-html.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts +6 -0
  72. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts.map +1 -1
  73. package/lib/typescript/module/api/client.d.ts +21 -1
  74. package/lib/typescript/module/api/client.d.ts.map +1 -1
  75. package/lib/typescript/module/i18n.d.ts +13 -0
  76. package/lib/typescript/module/i18n.d.ts.map +1 -1
  77. package/lib/typescript/module/index.d.ts +14 -1
  78. package/lib/typescript/module/index.d.ts.map +1 -1
  79. package/lib/typescript/module/locales.d.ts.map +1 -1
  80. package/lib/typescript/module/push/push-notifications.d.ts +26 -2
  81. package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
  82. package/lib/typescript/module/state/widget-store.d.ts +84 -1
  83. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  84. package/lib/typescript/module/types.d.ts +77 -0
  85. package/lib/typescript/module/types.d.ts.map +1 -1
  86. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/components/Post.d.ts +69 -0
  88. package/lib/typescript/module/ui/components/Post.d.ts.map +1 -0
  89. package/lib/typescript/module/ui/utils/html.d.ts +1 -0
  90. package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/utils/post-html.d.ts +54 -0
  92. package/lib/typescript/module/ui/utils/post-html.d.ts.map +1 -0
  93. package/lib/typescript/module/ui/utils/safe-url.d.ts +6 -0
  94. package/lib/typescript/module/ui/utils/safe-url.d.ts.map +1 -1
  95. package/package.json +1 -1
  96. package/src/api/client.ts +32 -0
  97. package/src/i18n.ts +14 -0
  98. package/src/index.tsx +17 -0
  99. package/src/locales.ts +13 -0
  100. package/src/push/push-notifications.ts +74 -8
  101. package/src/state/widget-store.ts +429 -6
  102. package/src/types.ts +77 -0
  103. package/src/ui/WidgetProvider.tsx +78 -2
  104. package/src/ui/components/Post.tsx +943 -0
  105. package/src/ui/utils/html.ts +1 -1
  106. package/src/ui/utils/post-html.ts +256 -0
  107. 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.handleTap(remoteMessage);
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.handleTap(remoteMessage);
266
+ if (remoteMessage) this.handleNotification(remoteMessage);
240
267
  })
241
268
  .catch(() => {});
242
269
  }
243
270
 
244
- /** Only widget pushes are ours — host app notifications pass through. */
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?.document_type !== 'widget_message') return;
248
- const conversationId = data?.document_id;
249
- if (typeof conversationId === 'string' && conversationId) {
250
- this.openConversation(conversationId);
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
  }