@repzo/widget-react-native 1.0.9 → 1.2.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 (191) hide show
  1. package/CHANGELOG.md +400 -0
  2. package/README.md +104 -5
  3. package/lib/commonjs/agent-ui-blocks.js +2 -1
  4. package/lib/commonjs/agent-ui-blocks.js.map +1 -1
  5. package/lib/commonjs/api/client.js +25 -0
  6. package/lib/commonjs/api/client.js.map +1 -1
  7. package/lib/commonjs/attachments/pickers.js +3 -6
  8. package/lib/commonjs/attachments/pickers.js.map +1 -1
  9. package/lib/commonjs/device-id.js +31 -3
  10. package/lib/commonjs/device-id.js.map +1 -1
  11. package/lib/commonjs/i18n.js +40 -4
  12. package/lib/commonjs/i18n.js.map +1 -1
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/locales.js +34 -1
  15. package/lib/commonjs/locales.js.map +1 -1
  16. package/lib/commonjs/push/push-notifications.js +9 -8
  17. package/lib/commonjs/push/push-notifications.js.map +1 -1
  18. package/lib/commonjs/state/widget-store.js +507 -26
  19. package/lib/commonjs/state/widget-store.js.map +1 -1
  20. package/lib/commonjs/storage.js +13 -1
  21. package/lib/commonjs/storage.js.map +1 -1
  22. package/lib/commonjs/types.js +20 -2
  23. package/lib/commonjs/types.js.map +1 -1
  24. package/lib/commonjs/ui/WidgetProvider.js +138 -49
  25. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  26. package/lib/commonjs/ui/components/AgentUiBlocks.js +35 -15
  27. package/lib/commonjs/ui/components/AgentUiBlocks.js.map +1 -1
  28. package/lib/commonjs/ui/components/Launcher.js +4 -1
  29. package/lib/commonjs/ui/components/Launcher.js.map +1 -1
  30. package/lib/commonjs/ui/components/Markdown.js +32 -2
  31. package/lib/commonjs/ui/components/Markdown.js.map +1 -1
  32. package/lib/commonjs/ui/components/Post.js +1241 -0
  33. package/lib/commonjs/ui/components/Post.js.map +1 -0
  34. package/lib/commonjs/ui/components/RatingCard.js +6 -3
  35. package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
  36. package/lib/commonjs/ui/hooks/use-widget-insets.js +0 -1
  37. package/lib/commonjs/ui/hooks/use-widget-insets.js.map +1 -1
  38. package/lib/commonjs/ui/screens/chat/ChatView.js +67 -29
  39. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  40. package/lib/commonjs/ui/utils/html.js +9 -1
  41. package/lib/commonjs/ui/utils/html.js.map +1 -1
  42. package/lib/commonjs/ui/utils/native-gradient.js +0 -1
  43. package/lib/commonjs/ui/utils/native-gradient.js.map +1 -1
  44. package/lib/commonjs/ui/utils/post-html.js +276 -0
  45. package/lib/commonjs/ui/utils/post-html.js.map +1 -0
  46. package/lib/commonjs/ui/utils/safe-url.js +16 -0
  47. package/lib/commonjs/ui/utils/safe-url.js.map +1 -1
  48. package/lib/module/agent-ui-blocks.js +2 -1
  49. package/lib/module/agent-ui-blocks.js.map +1 -1
  50. package/lib/module/api/client.js +25 -0
  51. package/lib/module/api/client.js.map +1 -1
  52. package/lib/module/attachments/pickers.js +3 -6
  53. package/lib/module/attachments/pickers.js.map +1 -1
  54. package/lib/module/device-id.js +31 -3
  55. package/lib/module/device-id.js.map +1 -1
  56. package/lib/module/i18n.js +40 -4
  57. package/lib/module/i18n.js.map +1 -1
  58. package/lib/module/index.js.map +1 -1
  59. package/lib/module/locales.js +34 -1
  60. package/lib/module/locales.js.map +1 -1
  61. package/lib/module/push/push-notifications.js +9 -8
  62. package/lib/module/push/push-notifications.js.map +1 -1
  63. package/lib/module/state/widget-store.js +507 -25
  64. package/lib/module/state/widget-store.js.map +1 -1
  65. package/lib/module/storage.js +13 -1
  66. package/lib/module/storage.js.map +1 -1
  67. package/lib/module/types.js +20 -2
  68. package/lib/module/types.js.map +1 -1
  69. package/lib/module/ui/WidgetProvider.js +139 -50
  70. package/lib/module/ui/WidgetProvider.js.map +1 -1
  71. package/lib/module/ui/components/AgentUiBlocks.js +35 -15
  72. package/lib/module/ui/components/AgentUiBlocks.js.map +1 -1
  73. package/lib/module/ui/components/Launcher.js +4 -1
  74. package/lib/module/ui/components/Launcher.js.map +1 -1
  75. package/lib/module/ui/components/Markdown.js +32 -3
  76. package/lib/module/ui/components/Markdown.js.map +1 -1
  77. package/lib/module/ui/components/Post.js +1236 -0
  78. package/lib/module/ui/components/Post.js.map +1 -0
  79. package/lib/module/ui/components/RatingCard.js +6 -3
  80. package/lib/module/ui/components/RatingCard.js.map +1 -1
  81. package/lib/module/ui/hooks/use-widget-insets.js +0 -1
  82. package/lib/module/ui/hooks/use-widget-insets.js.map +1 -1
  83. package/lib/module/ui/screens/chat/ChatView.js +68 -30
  84. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  85. package/lib/module/ui/utils/html.js +9 -2
  86. package/lib/module/ui/utils/html.js.map +1 -1
  87. package/lib/module/ui/utils/native-gradient.js +0 -1
  88. package/lib/module/ui/utils/native-gradient.js.map +1 -1
  89. package/lib/module/ui/utils/post-html.js +271 -0
  90. package/lib/module/ui/utils/post-html.js.map +1 -0
  91. package/lib/module/ui/utils/safe-url.js +15 -0
  92. package/lib/module/ui/utils/safe-url.js.map +1 -1
  93. package/lib/typescript/commonjs/agent-ui-blocks.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/api/client.d.ts +19 -1
  95. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/device-id.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/i18n.d.ts +35 -1
  99. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/index.d.ts +1 -1
  101. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/push/push-notifications.d.ts +6 -2
  104. package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/state/widget-store.d.ts +97 -8
  106. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/storage.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/types.d.ts +91 -8
  109. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts +14 -0
  112. package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/components/Markdown.d.ts +8 -0
  115. package/lib/typescript/commonjs/ui/components/Markdown.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/ui/components/Post.d.ts +69 -0
  117. package/lib/typescript/commonjs/ui/components/Post.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/ui/hooks/use-widget-insets.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  121. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +10 -0
  122. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  123. package/lib/typescript/commonjs/ui/utils/html.d.ts +1 -0
  124. package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/ui/utils/native-gradient.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/ui/utils/post-html.d.ts +54 -0
  127. package/lib/typescript/commonjs/ui/utils/post-html.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts +6 -0
  129. package/lib/typescript/commonjs/ui/utils/safe-url.d.ts.map +1 -1
  130. package/lib/typescript/module/agent-ui-blocks.d.ts.map +1 -1
  131. package/lib/typescript/module/api/client.d.ts +19 -1
  132. package/lib/typescript/module/api/client.d.ts.map +1 -1
  133. package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
  134. package/lib/typescript/module/device-id.d.ts.map +1 -1
  135. package/lib/typescript/module/i18n.d.ts +35 -1
  136. package/lib/typescript/module/i18n.d.ts.map +1 -1
  137. package/lib/typescript/module/index.d.ts +1 -1
  138. package/lib/typescript/module/index.d.ts.map +1 -1
  139. package/lib/typescript/module/locales.d.ts.map +1 -1
  140. package/lib/typescript/module/push/push-notifications.d.ts +6 -2
  141. package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
  142. package/lib/typescript/module/state/widget-store.d.ts +97 -8
  143. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  144. package/lib/typescript/module/storage.d.ts.map +1 -1
  145. package/lib/typescript/module/types.d.ts +91 -8
  146. package/lib/typescript/module/types.d.ts.map +1 -1
  147. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts +14 -0
  149. package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
  151. package/lib/typescript/module/ui/components/Markdown.d.ts +8 -0
  152. package/lib/typescript/module/ui/components/Markdown.d.ts.map +1 -1
  153. package/lib/typescript/module/ui/components/Post.d.ts +69 -0
  154. package/lib/typescript/module/ui/components/Post.d.ts.map +1 -0
  155. package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
  156. package/lib/typescript/module/ui/hooks/use-widget-insets.d.ts.map +1 -1
  157. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  158. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +10 -0
  159. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  160. package/lib/typescript/module/ui/utils/html.d.ts +1 -0
  161. package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
  162. package/lib/typescript/module/ui/utils/native-gradient.d.ts.map +1 -1
  163. package/lib/typescript/module/ui/utils/post-html.d.ts +54 -0
  164. package/lib/typescript/module/ui/utils/post-html.d.ts.map +1 -0
  165. package/lib/typescript/module/ui/utils/safe-url.d.ts +6 -0
  166. package/lib/typescript/module/ui/utils/safe-url.d.ts.map +1 -1
  167. package/package.json +14 -4
  168. package/src/agent-ui-blocks.ts +2 -1
  169. package/src/api/client.ts +27 -0
  170. package/src/attachments/pickers.ts +3 -6
  171. package/src/device-id.ts +34 -3
  172. package/src/i18n.ts +39 -2
  173. package/src/index.tsx +4 -0
  174. package/src/locales.ts +33 -0
  175. package/src/push/push-notifications.ts +10 -8
  176. package/src/state/widget-store.ts +480 -24
  177. package/src/storage.ts +18 -1
  178. package/src/types.ts +91 -8
  179. package/src/ui/WidgetProvider.tsx +166 -48
  180. package/src/ui/components/AgentUiBlocks.tsx +40 -12
  181. package/src/ui/components/Launcher.tsx +6 -1
  182. package/src/ui/components/Markdown.tsx +34 -3
  183. package/src/ui/components/Post.tsx +943 -0
  184. package/src/ui/components/RatingCard.tsx +6 -2
  185. package/src/ui/hooks/use-widget-insets.ts +0 -1
  186. package/src/ui/screens/chat/ChatView.tsx +77 -42
  187. package/src/ui/screens/chat/ChatView.types.ts +10 -0
  188. package/src/ui/utils/html.ts +10 -2
  189. package/src/ui/utils/native-gradient.ts +0 -1
  190. package/src/ui/utils/post-html.ts +256 -0
  191. package/src/ui/utils/safe-url.ts +15 -0
package/src/storage.ts CHANGED
@@ -44,7 +44,6 @@ function createMemoryAdapter(): RepzoWidgetStorageAdapter {
44
44
 
45
45
  function resolveAsyncStorage(): RepzoWidgetStorageAdapter | null {
46
46
  try {
47
- // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
48
47
  const mod = require('@react-native-async-storage/async-storage');
49
48
  const asyncStorage = mod?.default ?? mod;
50
49
  if (!asyncStorage?.getItem) return null;
@@ -101,10 +100,27 @@ export const Storage = {
101
100
 
102
101
  let secureAdapter: RepzoWidgetStorageAdapter | null = null;
103
102
 
103
+ let warnedPlaintext = false;
104
+
104
105
  export function setSecureStorageAdapter(adapter: RepzoWidgetStorageAdapter | null): void {
105
106
  secureAdapter = adapter;
106
107
  }
107
108
 
109
+ /**
110
+ * Development builds only, once per run: the bearer token is about to land on
111
+ * disk unencrypted. The in-memory fallback writes nothing, so it stays quiet.
112
+ */
113
+ function warnPlaintextOnce(): void {
114
+ if (warnedPlaintext || typeof __DEV__ === 'undefined' || !__DEV__) return;
115
+ if (activeAdapter() === memoryAdapter) return;
116
+ warnedPlaintext = true;
117
+ console.warn(
118
+ '[repzo-widget] no secure storage adapter is set, so the visitor token is stored in plain ' +
119
+ 'text. Call RepzoWidget.setSecureStorageAdapter() before boot() — see "Secure token ' +
120
+ 'storage" in the README.'
121
+ );
122
+ }
123
+
108
124
  export const SecureStorage = {
109
125
  async getItem(key: string): Promise<string | null> {
110
126
  if (!secureAdapter) return Storage.getItem(key);
@@ -126,6 +142,7 @@ export const SecureStorage = {
126
142
 
127
143
  async setItem(key: string, value: string): Promise<void> {
128
144
  if (!secureAdapter) {
145
+ warnPlaintextOnce();
129
146
  await Storage.setItem(key, value);
130
147
  return;
131
148
  }
package/src/types.ts CHANGED
@@ -164,12 +164,18 @@ export interface WidgetUser {
164
164
  /**
165
165
  * The user's account (company) in your app. Matched by `id`, so a renamed
166
166
  * account updates the same CRM account instead of creating another.
167
+ * @since 1.0.8
167
168
  */
168
169
  account?: WidgetUserAccount;
169
- /** When they signed up in your app — a Date, unix seconds/ms, or an ISO string. Shown as "Signed up" in the inbox. */
170
+ /**
171
+ * When they signed up in your app — a Date, unix seconds/ms, or an ISO
172
+ * string. Shown as "Signed up" in the inbox.
173
+ * @since 1.0.9
174
+ */
170
175
  signedUpAt?: Date | number | string;
171
176
  }
172
177
 
178
+ /** The user's account (company) in your app, sent as `WidgetUser.account`. @since 1.0.8 */
173
179
  export interface WidgetUserAccount {
174
180
  /** Your app's stable account id — required. */
175
181
  id: string;
@@ -237,6 +243,13 @@ export interface BootOptions {
237
243
  * `<RepzoWidgetProvider>` to be mounted.
238
244
  */
239
245
  inAppNotifications?: boolean;
246
+ /**
247
+ * Show Send posts from Workstation (Send → Post, "Show on: Mobile") over the
248
+ * app (default true): a card that opens into the full post, with replies or
249
+ * reactions. Requires `<RepzoWidgetProvider>`.
250
+ * @since 1.2.0
251
+ */
252
+ posts?: boolean;
240
253
  }
241
254
 
242
255
  /** An option in the composer's attach menu. */
@@ -490,13 +503,6 @@ export interface RepzoWidgetStorageAdapter {
490
503
  removeItem(key: string): Promise<void> | void;
491
504
  }
492
505
 
493
- /**
494
- * `RepzoWidgetError.code`. The last two come from the server's 403 body and
495
- * are terminal — the SDK stops realtime and does not retry on its own:
496
- * - `messenger_api_disabled` — the channel's Messenger API is switched off.
497
- * - `identity_verification_failed` — `userHash` missing/wrong while the
498
- * channel enforces identity verification.
499
- */
500
506
  /** A bad-rating reason on the rating card, labelled per language. */
501
507
  export interface RatingReason {
502
508
  id: string;
@@ -516,6 +522,13 @@ export interface ConversationRatingResult {
516
522
  locked: boolean;
517
523
  }
518
524
 
525
+ /**
526
+ * `RepzoWidgetError.code`. The last two come from the server's 403 body and
527
+ * are terminal — the SDK stops realtime and does not retry on its own:
528
+ * - `messenger_api_disabled` — the channel's Messenger API is switched off.
529
+ * - `identity_verification_failed` — `userHash` missing/wrong while the
530
+ * channel enforces identity verification.
531
+ */
519
532
  export type RepzoWidgetErrorCode =
520
533
  | 'network'
521
534
  | 'http'
@@ -563,6 +576,18 @@ export interface WidgetState {
563
576
  * app. Only set while the messenger is closed and its thread is unread.
564
577
  */
565
578
  inAppNotification: InAppNotification | null;
579
+ /**
580
+ * The Send post this visitor should see now (Send → Post shown on Mobile),
581
+ * drawn over the app. One at a time: the next one loads once this one is
582
+ * closed. Always `null` with `boot({ posts: false })`.
583
+ * @since 1.2.0
584
+ */
585
+ post: WidgetPost | null;
586
+ /**
587
+ * The post is open in full (tapped from its card, or sent as a full post).
588
+ * @since 1.2.0
589
+ */
590
+ postExpanded: boolean;
566
591
  }
567
592
 
568
593
  export interface InAppNotification {
@@ -574,3 +599,61 @@ export interface InAppNotification {
574
599
  preview: string;
575
600
  hasAttachments: boolean;
576
601
  }
602
+
603
+ /** What a visitor did with a post: saw it, opened it, tapped a link, reacted, replied, minimized or closed it. @since 1.2.0 */
604
+ export type PostInteractionKind = 'view' | 'open' | 'click' | 'reaction' | 'reply' | 'minimize' | 'dismiss';
605
+
606
+ /** Something the visitor already replied to a post (shown under it). @since 1.2.0 */
607
+ export interface PostReply {
608
+ text: string;
609
+ /** ISO time. */
610
+ at: string;
611
+ }
612
+
613
+ /**
614
+ * A Send post as Workstation sends it for this visitor: already personalized
615
+ * and sanitized down to paragraphs, bold/italic/code, links, lists, code
616
+ * blocks, a button (`div[data-post-button]`) and social links
617
+ * (`div[data-post-social]`). Fields the SDK doesn't recognize fall back to the
618
+ * plainest option.
619
+ * @since 1.2.0
620
+ */
621
+ export interface WidgetPost {
622
+ /** The Send campaign id. */
623
+ id: string;
624
+ bodyHtml: string;
625
+ /** First words of the post, for its card. */
626
+ snippet: string;
627
+ sender: { name: string; initials: string; avatarUrl: string | null } | null;
628
+ workspaceName: string;
629
+ /** `small` opens as a sheet, `large` as a full screen. */
630
+ format: 'small' | 'large';
631
+ replyType: 'text' | 'reaction' | 'none';
632
+ reactions: string[];
633
+ /** `badge`: a small pill; `snippet`: a card with the first words; `full`: opens right away. */
634
+ sentAs: 'badge' | 'snippet' | 'full';
635
+ /** The reaction this visitor already picked. */
636
+ reacted: string | null;
637
+ /** What they already replied, oldest first. */
638
+ replies: PostReply[];
639
+ /** They minimized it ("not now"): it waits as its badge or card instead of opening by itself. */
640
+ minimized: boolean;
641
+ /**
642
+ * The post's conversation with them, oldest first: their replies and the
643
+ * team's answers from the inbox. Older Workstation versions send only
644
+ * `replies`, which then fill it.
645
+ */
646
+ thread: PostThreadMessage[];
647
+ /** That conversation, to continue it in the messenger (null before their first reply). */
648
+ conversationId: string | null;
649
+ }
650
+
651
+ /** One message under a post: theirs, or a teammate's answer. @since 1.2.0 */
652
+ export interface PostThreadMessage {
653
+ from: 'person' | 'team';
654
+ text: string;
655
+ /** The teammate's name on team answers. */
656
+ senderName: string | null;
657
+ /** ISO time. */
658
+ at: string;
659
+ }
@@ -6,7 +6,7 @@
6
6
  * the modal (or never mounting it) never drops a conversation.
7
7
  */
8
8
 
9
- import { useCallback, useMemo } from 'react';
9
+ import { useCallback, useEffect, useMemo, useState } from 'react';
10
10
  import {
11
11
  ActivityIndicator,
12
12
  Modal,
@@ -29,16 +29,18 @@ import {
29
29
  import { controller } from '../controller';
30
30
  import { buildLabels, createTranslator, formatLabel, localizeWelcomeText } from '../i18n';
31
31
  import { useWidgetState } from '../state/use-widget-state';
32
- import type { AgentUiAction, OutgoingAttachment, WidgetSpaceType } from '../types';
32
+ import type { AgentUiAction, OutgoingAttachment, PostInteractionKind, WidgetSpaceType } from '../types';
33
33
  import { WidgetErrorBoundary } from './components/ErrorBoundary';
34
34
  import { AlertIcon } from './components/icons';
35
35
  import { InAppNotification } from './components/InAppNotification';
36
+ import { PostOverlay } from './components/Post';
36
37
  import { OutboundMessageCard } from './components/OutboundMessageCard';
37
38
  import { WidgetPanel } from './components/WidgetPanel';
38
39
  import { useKeyboardOverlap } from './hooks/use-keyboard-overlap';
39
40
  import { TopInsetContext } from './hooks/use-top-inset';
40
41
  import { useWidgetInsets } from './hooks/use-widget-insets';
41
42
  import { ChatView } from './screens/chat/ChatView';
43
+ import type { ChatLabels } from './screens/chat/ChatView.types';
42
44
  import { ConversationsList } from './screens/conversations/ConversationsList';
43
45
  import { HomeView, resolveHeaderTextColor } from './screens/home/HomeView';
44
46
  import { PreChatForm } from './screens/pre-chat/PreChatForm';
@@ -61,6 +63,9 @@ export interface RepzoWidgetProviderProps {
61
63
  presentationStyle?: 'pageSheet' | 'fullScreen';
62
64
  }
63
65
 
66
+ /** Longest the messenger's iOS slide-out is waited on when `onDismiss` doesn't arrive. */
67
+ const MESSENGER_DISMISS_FALLBACK_MS = 600;
68
+
64
69
  export function RepzoWidgetProvider({
65
70
  children,
66
71
  presentationStyle = 'pageSheet',
@@ -135,6 +140,17 @@ export function RepzoWidgetProvider({
135
140
  professional: labels.blockProfessional,
136
141
  priceOffer: labels.blockPriceOffer,
137
142
  working: labels.working,
143
+ subtotal: labels.blockSubtotal,
144
+ discount: labels.blockDiscount,
145
+ tax: labels.blockTax,
146
+ shipping: labels.blockShipping,
147
+ total: labels.blockTotal,
148
+ minutes: labels.blockMinutes,
149
+ save: labels.blockSave,
150
+ anyAvailable: labels.blockAnyAvailable,
151
+ withProvider: labels.blockWithProvider,
152
+ bookTime: labels.blockBookTime,
153
+ validUntil: labels.blockValidUntil,
138
154
  }),
139
155
  [labels]
140
156
  );
@@ -156,6 +172,62 @@ export function RepzoWidgetProvider({
156
172
  [labels]
157
173
  );
158
174
 
175
+ // Stable across renders so each message bubble only re-renders when its
176
+ // own message changes, not on every store update or keystroke.
177
+ const chatLabels = useMemo<ChatLabels>(
178
+ () => ({
179
+ back: labels.back,
180
+ loading: labels.loading,
181
+ loadEarlier: labels.loadEarlier,
182
+ emptyConversation: labels.emptyConversation,
183
+ messagePlaceholder: labels.messagePlaceholder,
184
+ failedToSend: labels.failedToSend,
185
+ thinking: labels.thinking,
186
+ team: labels.team,
187
+ statusOnline: labels.homeStatusOnline,
188
+ statusAway: labels.homeStatusAway,
189
+ close: labels.close,
190
+ today: labels.today,
191
+ yesterday: labels.yesterday,
192
+ conversationUpdated: labels.conversationUpdated,
193
+ messageDeleted: labels.messageDeleted,
194
+ messageSent: labels.messageSent,
195
+ messageSeen: labels.messageSeen,
196
+ waitingForTeammate: labels.waitingForTeammate,
197
+ joinedConversation: labels.joinedConversation,
198
+ procedureSend: labels.procedureSend,
199
+ procedureSent: labels.procedureSent,
200
+ procedureShareDetails: labels.procedureShareDetails,
201
+ procedureDetailsSent: labels.procedureDetailsSent,
202
+ procedureHide: labels.procedureHide,
203
+ procedureFieldCount: labels.procedureFieldCount,
204
+ procedureFieldCountOne: labels.procedureFieldCountOne,
205
+ procedureTitle: labels.procedureTitle,
206
+ procedureEmailPlaceholder: labels.procedureEmailPlaceholder,
207
+ procedurePhonePlaceholder: labels.procedurePhonePlaceholder,
208
+ blocks: blockLabels,
209
+ rating: {
210
+ question: labels.ratingQuestion,
211
+ reasonPrompt: labels.ratingReasonPrompt,
212
+ commentPlaceholder: labels.ratingCommentPlaceholder,
213
+ submit: labels.ratingSubmit,
214
+ done: labels.ratingDone,
215
+ thanks: labels.ratingThanks,
216
+ locked: labels.ratingLocked,
217
+ expired: labels.ratingExpired,
218
+ failed: labels.ratingFailed,
219
+ scale: [labels.rating1, labels.rating2, labels.rating3, labels.rating4, labels.rating5],
220
+ },
221
+ attach: attachLabels,
222
+ viewer: {
223
+ close: labels.close,
224
+ loadFailed: labels.imageLoadFailed,
225
+ openExternally: labels.openImageExternally,
226
+ },
227
+ }),
228
+ [labels, blockLabels, attachLabels]
229
+ );
230
+
159
231
  // Read per render: `boot({ attachments })` sets it outside React state.
160
232
  const attachmentSources = theme.attachmentsEnabled ? getAttachmentSources() : [];
161
233
 
@@ -167,6 +239,57 @@ export function RepzoWidgetProvider({
167
239
  );
168
240
  const handleDismissNotification = useCallback(() => store.dismissInAppNotification(), [store]);
169
241
 
242
+ // iOS can't present the post's modal while the messenger's is still sliding
243
+ // away (it would silently never show), so a post waits for that to finish.
244
+ // `onDismiss` is the signal; the timer covers React Native versions where it
245
+ // doesn't fire.
246
+ const [messengerOnScreen, setMessengerOnScreen] = useState(state.isOpen);
247
+ useEffect(() => {
248
+ if (state.isOpen || Platform.OS !== 'ios') {
249
+ setMessengerOnScreen(state.isOpen);
250
+ return undefined;
251
+ }
252
+ const timer = setTimeout(() => setMessengerOnScreen(false), MESSENGER_DISMISS_FALLBACK_MS);
253
+ return () => clearTimeout(timer);
254
+ }, [state.isOpen]);
255
+ const handleMessengerDismiss = useCallback(() => setMessengerOnScreen(false), []);
256
+
257
+ const handlePostShown = useCallback((campaignId: string) => store.postDidShow(campaignId), [store]);
258
+ const handlePostExpand = useCallback((campaignId: string) => store.expandPost(campaignId), [store]);
259
+ const handlePostMinimize = useCallback((campaignId: string) => store.minimizePost(campaignId), [store]);
260
+ const handlePostCollapse = useCallback((campaignId: string) => store.collapsePost(campaignId), [store]);
261
+ // The post steps aside first: iOS can't present the messenger while the post's modal is still up.
262
+ const handlePostOpenConversation = useCallback(
263
+ (campaignId: string, conversationId: string) => {
264
+ store.collapsePost(campaignId);
265
+ setTimeout(() => void store.openConversation(conversationId), Platform.OS === 'ios' ? 400 : 0);
266
+ },
267
+ [store]
268
+ );
269
+ const handlePostInteract = useCallback(
270
+ (campaignId: string, kind: Exclude<PostInteractionKind, 'view' | 'open' | 'minimize'>, value?: string) =>
271
+ store.recordPostInteraction(campaignId, kind, value),
272
+ [store]
273
+ );
274
+ const postLabels = useMemo(
275
+ () => ({
276
+ from: labels.postFrom,
277
+ newMessage: labels.postNewMessage,
278
+ openHint: labels.postOpenHint,
279
+ close: labels.postClose,
280
+ minimize: labels.postMinimize,
281
+ replyPlaceholder: labels.postReplyPlaceholder,
282
+ send: labels.postSend,
283
+ sent: labels.postSent,
284
+ replyFailed: labels.postReplyFailed,
285
+ reactions: labels.postReactions,
286
+ thanks: labels.postThanks,
287
+ conversation: labels.postConversation,
288
+ continueChat: labels.postContinueChat,
289
+ }),
290
+ [labels]
291
+ );
292
+
170
293
  const handleTabChange = useCallback((tab: WidgetSpaceType) => store.setActiveTab(tab), [store]);
171
294
 
172
295
  // Rejections reach ChatView, which keeps unsent attachments for a retry.
@@ -176,6 +299,21 @@ export function RepzoWidgetProvider({
176
299
  [store]
177
300
  );
178
301
 
302
+ const handleIdentifyVisitor = useCallback(
303
+ (data: { email?: string; name?: string; phone?: string }) => store.identifyVisitor(data),
304
+ [store]
305
+ );
306
+ const handleRateConversation = useCallback(
307
+ (
308
+ messageId: string,
309
+ ratingId: string,
310
+ data: { rating: number; reason?: string; comment?: string }
311
+ ) => store.rateConversation(messageId, ratingId, data),
312
+ [store]
313
+ );
314
+ const handleLoadMore = useCallback(() => void store.loadMoreMessages(), [store]);
315
+ const handleTyping = useCallback((isTyping: boolean) => store.notifyTyping(isTyping), [store]);
316
+
179
317
  const handlePreChatSubmit = useCallback(
180
318
  async (data: Record<string, string>) => {
181
319
  try {
@@ -298,55 +436,14 @@ export function RepzoWidgetProvider({
298
436
  defaultLocale={state.config?.defaultLocale}
299
437
  prefillText={state.prefillText}
300
438
  offlineMessage={theme.offlineMessage}
301
- labels={{
302
- back: labels.back,
303
- loading: labels.loading,
304
- loadEarlier: labels.loadEarlier,
305
- emptyConversation: labels.emptyConversation,
306
- messagePlaceholder: labels.messagePlaceholder,
307
- failedToSend: labels.failedToSend,
308
- thinking: labels.thinking,
309
- team: labels.team,
310
- statusOnline: labels.homeStatusOnline,
311
- statusAway: labels.homeStatusAway,
312
- close: labels.close,
313
- today: labels.today,
314
- yesterday: labels.yesterday,
315
- conversationUpdated: labels.conversationUpdated,
316
- messageDeleted: labels.messageDeleted,
317
- messageSent: labels.messageSent,
318
- messageSeen: labels.messageSeen,
319
- waitingForTeammate: labels.waitingForTeammate,
320
- joinedConversation: labels.joinedConversation,
321
- procedureSend: labels.procedureSend,
322
- procedureSent: labels.procedureSent,
323
- blocks: blockLabels,
324
- rating: {
325
- question: labels.ratingQuestion,
326
- reasonPrompt: labels.ratingReasonPrompt,
327
- commentPlaceholder: labels.ratingCommentPlaceholder,
328
- submit: labels.ratingSubmit,
329
- done: labels.ratingDone,
330
- thanks: labels.ratingThanks,
331
- locked: labels.ratingLocked,
332
- expired: labels.ratingExpired,
333
- failed: labels.ratingFailed,
334
- scale: [labels.rating1, labels.rating2, labels.rating3, labels.rating4, labels.rating5],
335
- },
336
- attach: attachLabels,
337
- viewer: {
338
- close: labels.close,
339
- loadFailed: labels.imageLoadFailed,
340
- openExternally: labels.openImageExternally,
341
- },
342
- }}
439
+ labels={chatLabels}
343
440
  attachmentSources={attachmentSources}
344
441
  onPickAttachments={pickAttachments}
345
442
  onSendMessage={handleSendMessage}
346
- onIdentifyVisitor={(data) => store.identifyVisitor(data)}
347
- onRateConversation={(messageId, ratingId, data) => store.rateConversation(messageId, ratingId, data)}
348
- onLoadMore={() => store.loadMoreMessages()}
349
- onTyping={(isTyping) => store.notifyTyping(isTyping)}
443
+ onIdentifyVisitor={handleIdentifyVisitor}
444
+ onRateConversation={handleRateConversation}
445
+ onLoadMore={handleLoadMore}
446
+ onTyping={handleTyping}
350
447
  onBack={handleBack}
351
448
  onClose={handleClose}
352
449
  />
@@ -421,6 +518,26 @@ export function RepzoWidgetProvider({
421
518
  return (
422
519
  <>
423
520
  {children}
521
+ {/* A post steps aside while the messenger is open, and its card while a reply card shows. */}
522
+ {state.post && !state.isOpen && !messengerOnScreen && (state.postExpanded || !state.inAppNotification) ? (
523
+ <WidgetErrorBoundary resetKey={state.post.id} fallback={() => null}>
524
+ <PostOverlay
525
+ // A new post remounts, so it reports its view.
526
+ key={state.post.id}
527
+ post={state.post}
528
+ expanded={state.postExpanded}
529
+ color={theme.primaryColor}
530
+ insets={insets}
531
+ labels={postLabels}
532
+ onShown={handlePostShown}
533
+ onExpand={handlePostExpand}
534
+ onMinimize={handlePostMinimize}
535
+ onCollapse={handlePostCollapse}
536
+ onOpenConversation={handlePostOpenConversation}
537
+ onInteract={handlePostInteract}
538
+ />
539
+ </WidgetErrorBoundary>
540
+ ) : null}
424
541
  {state.inAppNotification ? (
425
542
  <InAppNotification
426
543
  // A new reply remounts the card, so it slides in fresh.
@@ -444,6 +561,7 @@ export function RepzoWidgetProvider({
444
561
  animationType="slide"
445
562
  presentationStyle={Platform.OS === 'ios' ? presentationStyle : undefined}
446
563
  onRequestClose={handleClose}
564
+ onDismiss={handleMessengerDismiss}
447
565
  // Edge to edge on every Android version, so the insets below are the
448
566
  // only top spacing (a non-translucent modal used to add a second one).
449
567
  statusBarTranslucent>
@@ -8,6 +8,7 @@
8
8
  import { createContext, useContext, useState } from 'react';
9
9
  import { Image, Pressable, StyleSheet, Text, TextInput, View, type ViewStyle } from 'react-native';
10
10
 
11
+ import { formatLabel } from '../../i18n';
11
12
  import type { AgentUiAction, AgentUiBlock } from '../../types';
12
13
  import { fontSize, palette, radius, shadows, spacing, withAlpha } from '../styles/theme';
13
14
 
@@ -18,6 +19,20 @@ export interface AgentUiBlockLabels {
18
19
  professional: string;
19
20
  priceOffer: string;
20
21
  working: string;
22
+ subtotal: string;
23
+ discount: string;
24
+ tax: string;
25
+ shipping: string;
26
+ total: string;
27
+ /** `{count}` = minutes. */
28
+ minutes: string;
29
+ save: string;
30
+ anyAvailable: string;
31
+ /** `{name}` = the professional. */
32
+ withProvider: string;
33
+ bookTime: string;
34
+ /** `{date}` = the formatted date. */
35
+ validUntil: string;
21
36
  }
22
37
 
23
38
  const DEFAULT_BLOCK_LABELS: AgentUiBlockLabels = {
@@ -27,6 +42,17 @@ const DEFAULT_BLOCK_LABELS: AgentUiBlockLabels = {
27
42
  professional: 'Professional',
28
43
  priceOffer: 'Price offer',
29
44
  working: 'Working...',
45
+ subtotal: 'Subtotal',
46
+ discount: 'Discount',
47
+ tax: 'Tax',
48
+ shipping: 'Shipping',
49
+ total: 'Total',
50
+ minutes: '{count} min',
51
+ save: 'Save',
52
+ anyAvailable: 'Any available',
53
+ withProvider: 'With {name}',
54
+ bookTime: 'Book this time',
55
+ validUntil: 'Valid until {date}',
30
56
  };
31
57
 
32
58
  const BlockLabelContext = createContext<AgentUiBlockLabels>(DEFAULT_BLOCK_LABELS);
@@ -99,7 +125,7 @@ export function AgentUiBlocks({
99
125
  <View style={styles.row}>
100
126
  <Text style={styles.muted}>
101
127
  {block.product.durationMinutes
102
- ? `${block.product.durationMinutes} min`
128
+ ? formatLabel(labels.minutes, { count: block.product.durationMinutes })
103
129
  : block.product.unit || ''}
104
130
  </Text>
105
131
  <Text style={styles.price}>
@@ -182,23 +208,23 @@ export function AgentUiBlocks({
182
208
  ))}
183
209
  </View>
184
210
  <View style={styles.totals}>
185
- <MoneyRow label="Subtotal" value={formatMoney(block.subtotal, block.currency)} />
211
+ <MoneyRow label={labels.subtotal} value={formatMoney(block.subtotal, block.currency)} />
186
212
  {block.discount ? (
187
213
  <MoneyRow
188
- label="Discount"
214
+ label={labels.discount}
189
215
  value={`−${formatMoney(block.discount, block.currency)}`}
190
216
  />
191
217
  ) : null}
192
218
  {block.tax ? (
193
- <MoneyRow label="Tax" value={formatMoney(block.tax, block.currency)} />
219
+ <MoneyRow label={labels.tax} value={formatMoney(block.tax, block.currency)} />
194
220
  ) : null}
195
221
  {block.shipping ? (
196
222
  <MoneyRow
197
- label="Shipping"
223
+ label={labels.shipping}
198
224
  value={formatMoney(block.shipping, block.currency)}
199
225
  />
200
226
  ) : null}
201
- <MoneyRow label="Total" value={formatMoney(block.total, block.currency)} strong />
227
+ <MoneyRow label={labels.total} value={formatMoney(block.total, block.currency)} strong />
202
228
  </View>
203
229
  {block.action ? (
204
230
  <View style={styles.blockAction}>
@@ -314,7 +340,7 @@ export function AgentUiBlocks({
314
340
  hitSlop={8}>
315
341
  <Text
316
342
  style={[styles.linkButton, { color: checked ? color : palette.textSubtle }]}>
317
- Save
343
+ {labels.save}
318
344
  </Text>
319
345
  </Pressable>
320
346
  </View>
@@ -339,7 +365,7 @@ export function AgentUiBlocks({
339
365
  <View key={block.id} style={[styles.card, styles.cardPadded]}>
340
366
  <Text style={styles.title}>{labels.chooseTime}</Text>
341
367
  <Text style={styles.description}>
342
- {block.serviceName} · {block.durationMinutes} min
368
+ {block.serviceName} · {formatLabel(labels.minutes, { count: block.durationMinutes })}
343
369
  {block.timezone ? ` · ${block.timezone}` : ''}
344
370
  </Text>
345
371
 
@@ -351,7 +377,7 @@ export function AgentUiBlocks({
351
377
  selected={activeProviderId === 'any'}
352
378
  color={color}
353
379
  disabled={pending !== null}
354
- label="Any available"
380
+ label={labels.anyAvailable}
355
381
  onPress={() =>
356
382
  activeProviderId !== 'any' &&
357
383
  act({
@@ -425,7 +451,7 @@ export function AgentUiBlocks({
425
451
 
426
452
  {selectedProvider ? (
427
453
  <Text style={[styles.muted, styles.blockAction]}>
428
- With {selectedProvider.name}
454
+ {formatLabel(labels.withProvider, { name: selectedProvider.name })}
429
455
  </Text>
430
456
  ) : null}
431
457
  <View style={styles.blockAction}>
@@ -453,7 +479,7 @@ export function AgentUiBlocks({
453
479
  },
454
480
  })
455
481
  }>
456
- Book this time
482
+ {labels.bookTime}
457
483
  </ActionButton>
458
484
  </View>
459
485
  </View>
@@ -477,7 +503,9 @@ export function AgentUiBlocks({
477
503
  <Text style={styles.amount}>{formatMoney(block.amount, block.currency)}</Text>
478
504
  {block.validUntil ? (
479
505
  <Text style={styles.muted}>
480
- Valid until {formatDate(block.validUntil.slice(0, 10))}
506
+ {formatLabel(labels.validUntil, {
507
+ date: formatDate(block.validUntil.slice(0, 10)),
508
+ })}
481
509
  </Text>
482
510
  ) : null}
483
511
  {publicActions.map((action) => (
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { Pressable, StyleSheet, Text, View } from 'react-native';
11
11
 
12
+ import { createTranslator, DEFAULT_LABELS } from '../../i18n';
12
13
  import { useWidgetState } from '../../state/use-widget-state';
13
14
  import { controller } from '../../controller';
14
15
  import { ChatIcon, ChevronIcon } from './icons';
@@ -47,6 +48,10 @@ export function Launcher({
47
48
  const inset = Math.max(horizontalPadding ?? config?.launcherHorizontalPadding ?? 20, 20);
48
49
  const text = config?.bubbleText;
49
50
  const showText = !!text && !state.isOpen;
51
+ const translate = createTranslator(config, state.locale);
52
+ const accessibilityLabel = state.isOpen
53
+ ? translate('launcherClose', DEFAULT_LABELS.launcherClose)
54
+ : translate('launcherOpen', DEFAULT_LABELS.launcherOpen);
50
55
 
51
56
  return (
52
57
  <View
@@ -55,7 +60,7 @@ export function Launcher({
55
60
  <Pressable
56
61
  onPress={() => controller.store.toggle()}
57
62
  accessibilityRole="button"
58
- accessibilityLabel={state.isOpen ? 'Close chat' : 'Open chat'}
63
+ accessibilityLabel={accessibilityLabel}
59
64
  style={({ pressed }) => [
60
65
  styles.bubble,
61
66
  showText ? styles.bubbleWithText : styles.bubbleRound,