@repzo/widget-react-native 1.0.5 → 1.0.6

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 (74) hide show
  1. package/lib/commonjs/api/client.js +8 -0
  2. package/lib/commonjs/api/client.js.map +1 -1
  3. package/lib/commonjs/i18n.js +14 -0
  4. package/lib/commonjs/i18n.js.map +1 -1
  5. package/lib/commonjs/locales.js +13 -0
  6. package/lib/commonjs/locales.js.map +1 -1
  7. package/lib/commonjs/state/widget-store.js +25 -0
  8. package/lib/commonjs/state/widget-store.js.map +1 -1
  9. package/lib/commonjs/types.js +3 -0
  10. package/lib/commonjs/types.js.map +1 -1
  11. package/lib/commonjs/ui/WidgetProvider.js +13 -0
  12. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  13. package/lib/commonjs/ui/components/RatingCard.js +347 -0
  14. package/lib/commonjs/ui/components/RatingCard.js.map +1 -0
  15. package/lib/commonjs/ui/screens/chat/ChatView.js +22 -1
  16. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  17. package/lib/module/api/client.js +8 -0
  18. package/lib/module/api/client.js.map +1 -1
  19. package/lib/module/i18n.js +14 -0
  20. package/lib/module/i18n.js.map +1 -1
  21. package/lib/module/locales.js +13 -0
  22. package/lib/module/locales.js.map +1 -1
  23. package/lib/module/state/widget-store.js +25 -0
  24. package/lib/module/state/widget-store.js.map +1 -1
  25. package/lib/module/types.js +3 -0
  26. package/lib/module/types.js.map +1 -1
  27. package/lib/module/ui/WidgetProvider.js +13 -0
  28. package/lib/module/ui/WidgetProvider.js.map +1 -1
  29. package/lib/module/ui/components/RatingCard.js +341 -0
  30. package/lib/module/ui/components/RatingCard.js.map +1 -0
  31. package/lib/module/ui/screens/chat/ChatView.js +22 -1
  32. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  33. package/lib/typescript/commonjs/api/client.d.ts +7 -1
  34. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/i18n.d.ts +13 -0
  36. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/state/widget-store.d.ts +11 -1
  39. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/types.d.ts +17 -0
  41. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/ui/components/RatingCard.d.ts +57 -0
  44. package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -0
  45. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
  46. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +11 -1
  48. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  49. package/lib/typescript/module/api/client.d.ts +7 -1
  50. package/lib/typescript/module/api/client.d.ts.map +1 -1
  51. package/lib/typescript/module/i18n.d.ts +13 -0
  52. package/lib/typescript/module/i18n.d.ts.map +1 -1
  53. package/lib/typescript/module/locales.d.ts.map +1 -1
  54. package/lib/typescript/module/state/widget-store.d.ts +11 -1
  55. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  56. package/lib/typescript/module/types.d.ts +17 -0
  57. package/lib/typescript/module/types.d.ts.map +1 -1
  58. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  59. package/lib/typescript/module/ui/components/RatingCard.d.ts +57 -0
  60. package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -0
  61. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
  62. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  63. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +11 -1
  64. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/api/client.ts +13 -0
  67. package/src/i18n.ts +14 -0
  68. package/src/locales.ts +13 -0
  69. package/src/state/widget-store.ts +34 -0
  70. package/src/types.ts +19 -0
  71. package/src/ui/WidgetProvider.tsx +13 -0
  72. package/src/ui/components/RatingCard.tsx +355 -0
  73. package/src/ui/screens/chat/ChatView.tsx +26 -0
  74. package/src/ui/screens/chat/ChatView.types.ts +11 -0
@@ -8,5 +8,5 @@
8
8
  * `attachments/pickers.ts`).
9
9
  */
10
10
  import type { ChatViewProps } from './ChatView.types';
11
- export declare function ChatView({ messages, loading, hasMoreMessages, agentTyping, color, agentName, agentAvatar, agentOnline, isOnline, teamName, teamAvatars, replyTimeLabel, locale, prefillText, offlineMessage, labels, attachmentSources, onPickAttachments, onSendMessage, onIdentifyVisitor, onLoadMore, onTyping, onBack, onClose, }: ChatViewProps): import("react").JSX.Element;
11
+ export declare function ChatView({ messages, loading, hasMoreMessages, agentTyping, color, agentName, agentAvatar, agentOnline, isOnline, teamName, teamAvatars, replyTimeLabel, locale, prefillText, offlineMessage, labels, attachmentSources, onPickAttachments, onSendMessage, onIdentifyVisitor, onRateConversation, defaultLocale, onLoadMore, onTyping, onBack, onClose, }: ChatViewProps): import("react").JSX.Element;
12
12
  //# sourceMappingURL=ChatView.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BA8hBf"}
1
+ {"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAsDH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BAgiBf"}
@@ -1,9 +1,10 @@
1
1
  import type { TeamAvatar } from '../../../types';
2
2
  /** Props and label bundle for the chat thread. */
3
3
  import type { DraftAttachment, PickResult } from '../../../attachments/pickers';
4
- import type { AgentUiAction, AttachmentSource, Message, OutgoingAttachment } from '../../../types';
4
+ import type { AgentUiAction, AttachmentSource, ConversationRatingResult, Message, OutgoingAttachment } from '../../../types';
5
5
  import type { AgentUiBlockLabels } from '../../components/AgentUiBlocks';
6
6
  import type { ImageViewerLabels } from '../../components/ImageViewer';
7
+ import type { RatingCardLabels } from '../../components/RatingCard';
7
8
  export interface ChatLabels {
8
9
  back: string;
9
10
  loading: string;
@@ -27,6 +28,7 @@ export interface ChatLabels {
27
28
  procedureSend: string;
28
29
  procedureSent: string;
29
30
  blocks: AgentUiBlockLabels;
31
+ rating: RatingCardLabels;
30
32
  attach: AttachLabels;
31
33
  viewer: ImageViewerLabels;
32
34
  }
@@ -79,6 +81,14 @@ export interface ChatViewProps {
79
81
  name?: string;
80
82
  phone?: string;
81
83
  }) => Promise<void>;
84
+ /** Answers a rating card; rejects with the server error (409 locked, 410 expired). */
85
+ onRateConversation?: (messageId: string, ratingId: string, data: {
86
+ rating: number;
87
+ reason?: string;
88
+ comment?: string;
89
+ }) => Promise<ConversationRatingResult>;
90
+ /** The widget's default language — rating reasons fall back to it. */
91
+ defaultLocale?: string;
82
92
  onLoadMore: () => void;
83
93
  onTyping: (isTyping: boolean) => void;
84
94
  onBack: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repzo/widget-react-native",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Repzo chat widget for React Native \u2014 pure JavaScript, no native modules",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/commonjs/index.js",
package/src/api/client.ts CHANGED
@@ -12,6 +12,7 @@ import { SecureStorage, TOKEN_KEY } from '../storage';
12
12
  import type {
13
13
  AgentUiAction,
14
14
  Conversation,
15
+ ConversationRatingResult,
15
16
  InitResponse,
16
17
  Message,
17
18
  OutboundMessage,
@@ -285,6 +286,18 @@ export class WidgetApiClient {
285
286
  );
286
287
  }
287
288
 
289
+ /** Answers (or changes) the rating card posted after a teammate closed the chat. */
290
+ rateConversation(
291
+ conversationId: string,
292
+ ratingId: string,
293
+ data: { rating: number; reason?: string | null; comment?: string | null }
294
+ ): Promise<ConversationRatingResult> {
295
+ return this.request(
296
+ `/api/widget/conversations/${encodeURIComponent(conversationId)}/ratings/${encodeURIComponent(ratingId)}`,
297
+ { method: 'POST', body: data }
298
+ );
299
+ }
300
+
288
301
  identify(data: {
289
302
  email?: string;
290
303
  name?: string;
package/src/i18n.ts CHANGED
@@ -118,6 +118,20 @@ export const DEFAULT_LABELS = {
118
118
  messageSeen: 'Seen',
119
119
  waitingForTeammate: 'Waiting for a teammate',
120
120
  joinedConversation: 'joined the conversation',
121
+ // Conversation rating card, posted after a teammate closes the chat.
122
+ ratingQuestion: 'How would you rate this conversation?',
123
+ ratingReasonPrompt: 'What went wrong?',
124
+ ratingCommentPlaceholder: 'Tell us more (optional)',
125
+ ratingSubmit: 'Submit',
126
+ ratingThanks: 'Thanks for your feedback!',
127
+ ratingLocked: 'This rating can no longer be changed',
128
+ ratingExpired: 'This conversation can no longer be rated',
129
+ ratingFailed: "Couldn't save your rating. Please try again.",
130
+ rating1: 'Terrible',
131
+ rating2: 'Bad',
132
+ rating3: 'OK',
133
+ rating4: 'Great',
134
+ rating5: 'Amazing',
121
135
  // Forms.
122
136
  procedureSend: 'Send details',
123
137
  procedureSent: 'Sent',
package/src/locales.ts CHANGED
@@ -74,6 +74,19 @@ const ar: LabelMap = {
74
74
  messageSeen: 'تمت المشاهدة',
75
75
  waitingForTeammate: 'بانتظار أحد أعضاء الفريق',
76
76
  joinedConversation: 'انضم إلى المحادثة',
77
+ ratingQuestion: 'كيف تقيّم هذه المحادثة؟',
78
+ ratingReasonPrompt: 'ما الذي لم يكن جيداً؟',
79
+ ratingCommentPlaceholder: 'أخبرنا المزيد (اختياري)',
80
+ ratingSubmit: 'إرسال',
81
+ ratingThanks: 'شكراً على ملاحظاتك!',
82
+ ratingLocked: 'لم يعد بالإمكان تغيير هذا التقييم',
83
+ ratingExpired: 'لم يعد بالإمكان تقييم هذه المحادثة',
84
+ ratingFailed: 'تعذّر حفظ تقييمك. حاول مرة أخرى.',
85
+ rating1: 'سيئ جداً',
86
+ rating2: 'سيئ',
87
+ rating3: 'مقبول',
88
+ rating4: 'جيد',
89
+ rating5: 'ممتاز',
77
90
  procedureSend: 'إرسال التفاصيل',
78
91
  procedureSent: 'تم الإرسال',
79
92
  fieldRequired: '{field} مطلوب',
@@ -22,6 +22,7 @@ import type {
22
22
  AgentUiAction,
23
23
  BootOptions,
24
24
  Conversation,
25
+ ConversationRatingResult,
25
26
  Message,
26
27
  OutgoingAttachment,
27
28
  RepzoWidgetErrorCode,
@@ -1850,6 +1851,39 @@ export class WidgetStore {
1850
1851
  await this.client.identify(data);
1851
1852
  }
1852
1853
 
1854
+ /**
1855
+ * Answers a rating card in the thread on screen and mirrors the answer onto
1856
+ * its message, so the card keeps it across a re-render. Rejects with the
1857
+ * server's error (409 locked, 410 expired) for the card to show.
1858
+ */
1859
+ async rateConversation(
1860
+ messageId: string,
1861
+ ratingId: string,
1862
+ data: { rating: number; reason?: string | null; comment?: string | null }
1863
+ ): Promise<ConversationRatingResult> {
1864
+ const conversationId = this.state.activeConversationId;
1865
+ if (!conversationId) throw new RepzoWidgetError('No conversation is open', 0, 'http');
1866
+ const result = await this.client.rateConversation(conversationId, ratingId, data);
1867
+ if (this.state.activeConversationId === conversationId) {
1868
+ this.setState((current) => ({
1869
+ messages: current.messages.map((message) =>
1870
+ message.id === messageId
1871
+ ? {
1872
+ ...message,
1873
+ metadata: {
1874
+ ...message.metadata,
1875
+ rating: result.rating,
1876
+ ratingReason: result.reason,
1877
+ ratingComment: result.comment,
1878
+ },
1879
+ }
1880
+ : message
1881
+ ),
1882
+ }));
1883
+ }
1884
+ return result;
1885
+ }
1886
+
1853
1887
  /** Fire-and-forget visitor typing ping, throttled well under the 30/min cap. */
1854
1888
  notifyTyping(isTyping: boolean): void {
1855
1889
  const conversationId = this.state.activeConversationId;
package/src/types.ts CHANGED
@@ -480,6 +480,25 @@ export interface RepzoWidgetStorageAdapter {
480
480
  * - `identity_verification_failed` — `userHash` missing/wrong while the
481
481
  * channel enforces identity verification.
482
482
  */
483
+ /** A bad-rating reason on the rating card, labelled per language. */
484
+ export interface RatingReason {
485
+ id: string;
486
+ /** Locale code → label. */
487
+ labels: Record<string, string>;
488
+ }
489
+
490
+ /** The visitor's answer to a conversation rating card. */
491
+ export interface ConversationRatingResult {
492
+ id: string;
493
+ /** 1 (Terrible) … 5 (Amazing). */
494
+ rating: number;
495
+ /** Id of the reason picked after a bad rating (1–2), if any. */
496
+ reason: string | null;
497
+ comment: string | null;
498
+ /** A comment was left: the rating can't change any more. */
499
+ locked: boolean;
500
+ }
501
+
483
502
  export type RepzoWidgetErrorCode =
484
503
  | 'network'
485
504
  | 'http'
@@ -295,6 +295,7 @@ export function RepzoWidgetProvider({
295
295
  teamAvatars={teamAvatars}
296
296
  replyTimeLabel={theme.replyTimeLabel}
297
297
  locale={state.locale}
298
+ defaultLocale={state.config?.defaultLocale}
298
299
  prefillText={state.prefillText}
299
300
  offlineMessage={theme.offlineMessage}
300
301
  labels={{
@@ -320,6 +321,17 @@ export function RepzoWidgetProvider({
320
321
  procedureSend: labels.procedureSend,
321
322
  procedureSent: labels.procedureSent,
322
323
  blocks: blockLabels,
324
+ rating: {
325
+ question: labels.ratingQuestion,
326
+ reasonPrompt: labels.ratingReasonPrompt,
327
+ commentPlaceholder: labels.ratingCommentPlaceholder,
328
+ submit: labels.ratingSubmit,
329
+ thanks: labels.ratingThanks,
330
+ locked: labels.ratingLocked,
331
+ expired: labels.ratingExpired,
332
+ failed: labels.ratingFailed,
333
+ scale: [labels.rating1, labels.rating2, labels.rating3, labels.rating4, labels.rating5],
334
+ },
323
335
  attach: attachLabels,
324
336
  viewer: {
325
337
  close: labels.close,
@@ -331,6 +343,7 @@ export function RepzoWidgetProvider({
331
343
  onPickAttachments={pickAttachments}
332
344
  onSendMessage={handleSendMessage}
333
345
  onIdentifyVisitor={(data) => store.identifyVisitor(data)}
346
+ onRateConversation={(messageId, ratingId, data) => store.rateConversation(messageId, ratingId, data)}
334
347
  onLoadMore={() => store.loadMoreMessages()}
335
348
  onTyping={(isTyping) => store.notifyTyping(isTyping)}
336
349
  onBack={handleBack}
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Conversation rating card (CSAT), posted two minutes after a teammate closes
3
+ * the chat. Port of `client/src/widget/components/RatingCard.tsx`.
4
+ *
5
+ * Tapping a face saves it right away; after 😠/🙁 the visitor can also tap a
6
+ * reason, labelled in their language. A comment locks the rating. The server enforces the change window and answers
7
+ * 409 (locked) or 410 (expired), which the card shows instead of the faces'
8
+ * actions.
9
+ */
10
+
11
+ import { useState } from 'react';
12
+ import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
13
+
14
+ import type { ConversationRatingResult, RatingReason } from '../../types';
15
+ import { contrastingTextColor, fontSize, palette, radius, spacing, withAlpha } from '../styles/theme';
16
+
17
+ /** 1..5, worst to best — Intercom's five faces. */
18
+ const RATING_SCALE = [
19
+ { value: 1, emoji: '😠' },
20
+ { value: 2, emoji: '🙁' },
21
+ { value: 3, emoji: '😐' },
22
+ { value: 4, emoji: '🙂' },
23
+ { value: 5, emoji: '🤩' },
24
+ ] as const;
25
+
26
+ export interface RatingCardLabels {
27
+ question: string;
28
+ reasonPrompt: string;
29
+ commentPlaceholder: string;
30
+ submit: string;
31
+ thanks: string;
32
+ locked: string;
33
+ expired: string;
34
+ failed: string;
35
+ /** Accessibility names for the faces, worst to best. */
36
+ scale: [string, string, string, string, string];
37
+ }
38
+
39
+ /** 1–2 asks for a reason; 3 is neutral, 4–5 good. */
40
+ export const isBadRating = (rating: number | null) => rating != null && rating <= 2;
41
+
42
+ const isRatingReason = (value: unknown): value is RatingReason =>
43
+ !!value &&
44
+ typeof value === 'object' &&
45
+ typeof (value as RatingReason).id === 'string' &&
46
+ !!(value as RatingReason).labels &&
47
+ typeof (value as RatingReason).labels === 'object';
48
+
49
+ /** The rating card carried by a system message's metadata, or null. */
50
+ export function getRatingRequest(metadata: Record<string, unknown> | null | undefined) {
51
+ if (!metadata || metadata.event !== 'rating_request' || typeof metadata.ratingId !== 'string') return null;
52
+ const rating = typeof metadata.rating === 'number' && metadata.rating >= 1 && metadata.rating <= 5 ? metadata.rating : null;
53
+ const comment = typeof metadata.ratingComment === 'string' && metadata.ratingComment ? metadata.ratingComment : null;
54
+ const reason = typeof metadata.ratingReason === 'string' && metadata.ratingReason ? metadata.ratingReason : null;
55
+ const reasons = Array.isArray(metadata.ratingReasons) ? metadata.ratingReasons.filter(isRatingReason) : [];
56
+ return { ratingId: metadata.ratingId, rating, comment, reason, reasons };
57
+ }
58
+
59
+ /**
60
+ * A reason's label in the first of `locales` it has (full code, then its base
61
+ * language: "ar-SA" → "ar"), else English, else whatever it has.
62
+ */
63
+ export function reasonLabel(reason: RatingReason, locales: Array<string | null | undefined>): string {
64
+ for (const locale of [...locales, 'en']) {
65
+ if (!locale) continue;
66
+ const label = reason.labels[locale] || reason.labels[locale.split('-')[0]!];
67
+ if (label) return label;
68
+ }
69
+ return Object.values(reason.labels).find(Boolean) ?? reason.id;
70
+ }
71
+
72
+ interface RatingCardProps {
73
+ ratingId: string;
74
+ initialRating: number | null;
75
+ initialComment: string | null;
76
+ initialReason: string | null;
77
+ /** Offered after a bad rating (1–2). */
78
+ reasons: RatingReason[];
79
+ /** The visitor's language first, then the widget's default. */
80
+ locales: Array<string | null | undefined>;
81
+ color: string;
82
+ labels: RatingCardLabels;
83
+ onRate?: (
84
+ ratingId: string,
85
+ data: { rating: number; reason?: string; comment?: string }
86
+ ) => Promise<ConversationRatingResult>;
87
+ }
88
+
89
+ export function RatingCard({
90
+ ratingId,
91
+ initialRating,
92
+ initialComment,
93
+ initialReason,
94
+ reasons,
95
+ locales,
96
+ color,
97
+ labels,
98
+ onRate,
99
+ }: RatingCardProps) {
100
+ const [rating, setRating] = useState<number | null>(initialRating);
101
+ const [reason, setReason] = useState<string | null>(initialReason);
102
+ const [comment, setComment] = useState<string | null>(initialComment);
103
+ const [draft, setDraft] = useState('');
104
+ const [pending, setPending] = useState(false);
105
+ const [closedReason, setClosedReason] = useState<'locked' | 'expired' | null>(initialComment ? 'locked' : null);
106
+ const [failed, setFailed] = useState(false);
107
+ const interactive = !!onRate && !closedReason && !pending;
108
+ const canSubmit = !pending && draft.trim().length > 0;
109
+
110
+ const submit = async (next: { rating: number; reason?: string; comment?: string }) => {
111
+ if (!onRate || pending) return;
112
+ setPending(true);
113
+ setFailed(false);
114
+ try {
115
+ const result = await onRate(ratingId, next);
116
+ setRating(result.rating);
117
+ setReason(result.reason ?? null);
118
+ setComment(result.comment);
119
+ if (result.locked) setClosedReason('locked');
120
+ setDraft('');
121
+ } catch (error) {
122
+ const status = (error as { status?: number }).status;
123
+ if (status === 409) setClosedReason('locked');
124
+ else if (status === 410) setClosedReason('expired');
125
+ else setFailed(true);
126
+ } finally {
127
+ setPending(false);
128
+ }
129
+ };
130
+
131
+ const status =
132
+ closedReason === 'expired'
133
+ ? labels.expired
134
+ : closedReason === 'locked' && !comment
135
+ ? labels.locked
136
+ : failed
137
+ ? labels.failed
138
+ : rating != null
139
+ ? labels.thanks
140
+ : null;
141
+
142
+ return (
143
+ <View style={styles.row}>
144
+ <View style={styles.card} testID="repzo-rating-card">
145
+ <Text style={styles.question}>{labels.question}</Text>
146
+ <View style={styles.faces} accessibilityRole="radiogroup">
147
+ {RATING_SCALE.map((entry, index) => {
148
+ const selected = rating === entry.value;
149
+ return (
150
+ <Pressable
151
+ key={entry.value}
152
+ accessibilityRole="radio"
153
+ accessibilityLabel={labels.scale[index]}
154
+ accessibilityState={{ checked: selected, disabled: !interactive }}
155
+ disabled={!interactive}
156
+ hitSlop={4}
157
+ onPress={() => {
158
+ if (!selected) void submit({ rating: entry.value });
159
+ }}
160
+ style={({ pressed }) => [
161
+ styles.face,
162
+ selected && { borderColor: color, backgroundColor: withAlpha(color, 0.08) },
163
+ rating != null && !selected && styles.faceDimmed,
164
+ pressed && styles.facePressed,
165
+ ]}>
166
+ <Text style={styles.emoji}>{entry.emoji}</Text>
167
+ </Pressable>
168
+ );
169
+ })}
170
+ </View>
171
+
172
+ {status ? <Text style={[styles.status, failed && styles.statusFailed]}>{status}</Text> : null}
173
+
174
+ {isBadRating(rating) && reasons.length > 0 && (interactive || reason) ? (
175
+ <View style={styles.reasons}>
176
+ {interactive ? <Text style={styles.reasonPrompt}>{labels.reasonPrompt}</Text> : null}
177
+ <View style={styles.chips} accessibilityRole="radiogroup" accessibilityLabel={labels.reasonPrompt}>
178
+ {reasons
179
+ .filter((item) => interactive || item.id === reason)
180
+ .map((item) => {
181
+ const selected = item.id === reason;
182
+ return (
183
+ <Pressable
184
+ key={item.id}
185
+ accessibilityRole="radio"
186
+ accessibilityState={{ checked: selected, disabled: !interactive }}
187
+ disabled={!interactive}
188
+ onPress={() => {
189
+ if (!selected && rating != null) void submit({ rating, reason: item.id });
190
+ }}
191
+ style={({ pressed }) => [
192
+ styles.chip,
193
+ selected && { backgroundColor: color, borderColor: color },
194
+ pressed && styles.facePressed,
195
+ ]}>
196
+ <Text style={[styles.chipText, selected && { color: contrastingTextColor(color) }]}>
197
+ {reasonLabel(item, locales)}
198
+ </Text>
199
+ </Pressable>
200
+ );
201
+ })}
202
+ </View>
203
+ </View>
204
+ ) : null}
205
+
206
+ {comment ? (
207
+ <Text style={styles.comment}>{comment}</Text>
208
+ ) : rating != null && !closedReason ? (
209
+ <View style={styles.form}>
210
+ <TextInput
211
+ value={draft}
212
+ onChangeText={setDraft}
213
+ editable={!pending}
214
+ multiline
215
+ maxLength={2000}
216
+ placeholder={labels.commentPlaceholder}
217
+ placeholderTextColor={palette.textSubtle}
218
+ style={styles.input}
219
+ />
220
+ <Pressable
221
+ accessibilityRole="button"
222
+ disabled={!canSubmit}
223
+ onPress={() => {
224
+ const text = draft.trim();
225
+ if (text && rating != null) void submit({ rating, comment: text });
226
+ }}
227
+ style={({ pressed }) => [
228
+ styles.submit,
229
+ { backgroundColor: color },
230
+ !canSubmit && styles.submitDisabled,
231
+ pressed && canSubmit && styles.facePressed,
232
+ ]}>
233
+ <Text style={[styles.submitText, { color: contrastingTextColor(color) }]}>{labels.submit}</Text>
234
+ </Pressable>
235
+ </View>
236
+ ) : null}
237
+ </View>
238
+ </View>
239
+ );
240
+ }
241
+
242
+ const styles = StyleSheet.create({
243
+ row: {
244
+ alignItems: 'center',
245
+ paddingVertical: spacing.md,
246
+ },
247
+ card: {
248
+ width: '100%',
249
+ maxWidth: 340,
250
+ gap: spacing.md,
251
+ padding: spacing.lg,
252
+ borderRadius: radius.xl,
253
+ borderWidth: StyleSheet.hairlineWidth,
254
+ borderColor: palette.border,
255
+ backgroundColor: palette.white,
256
+ },
257
+ question: {
258
+ textAlign: 'center',
259
+ color: palette.ink,
260
+ fontSize: fontSize.body,
261
+ fontWeight: '600',
262
+ },
263
+ faces: {
264
+ flexDirection: 'row',
265
+ justifyContent: 'center',
266
+ gap: spacing.sm,
267
+ },
268
+ face: {
269
+ width: 48,
270
+ height: 48,
271
+ borderRadius: 24,
272
+ borderWidth: 2,
273
+ borderColor: 'transparent',
274
+ alignItems: 'center',
275
+ justifyContent: 'center',
276
+ },
277
+ faceDimmed: {
278
+ opacity: 0.45,
279
+ },
280
+ facePressed: {
281
+ opacity: 0.7,
282
+ },
283
+ emoji: {
284
+ fontSize: 28,
285
+ },
286
+ status: {
287
+ textAlign: 'center',
288
+ color: palette.textMuted,
289
+ fontSize: fontSize.meta,
290
+ },
291
+ statusFailed: {
292
+ color: palette.danger,
293
+ },
294
+ reasons: {
295
+ gap: spacing.sm,
296
+ },
297
+ reasonPrompt: {
298
+ color: palette.ink,
299
+ fontSize: fontSize.meta,
300
+ fontWeight: '600',
301
+ },
302
+ chips: {
303
+ flexDirection: 'row',
304
+ flexWrap: 'wrap',
305
+ gap: spacing.sm,
306
+ },
307
+ chip: {
308
+ paddingHorizontal: spacing.md,
309
+ paddingVertical: spacing.sm,
310
+ borderRadius: radius.pill,
311
+ borderWidth: 1,
312
+ borderColor: palette.border,
313
+ backgroundColor: palette.white,
314
+ },
315
+ chipText: {
316
+ color: palette.ink,
317
+ fontSize: fontSize.meta,
318
+ },
319
+ comment: {
320
+ paddingHorizontal: spacing.md,
321
+ paddingVertical: spacing.sm,
322
+ borderRadius: radius.md,
323
+ backgroundColor: palette.surfaceAlt,
324
+ color: palette.ink,
325
+ fontSize: fontSize.meta,
326
+ writingDirection: 'auto',
327
+ },
328
+ form: {
329
+ gap: spacing.sm,
330
+ },
331
+ input: {
332
+ minHeight: 64,
333
+ paddingHorizontal: spacing.md,
334
+ paddingVertical: spacing.sm,
335
+ borderRadius: radius.md,
336
+ borderWidth: 1,
337
+ borderColor: palette.border,
338
+ color: palette.ink,
339
+ fontSize: fontSize.input,
340
+ textAlignVertical: 'top',
341
+ },
342
+ submit: {
343
+ alignSelf: 'flex-end',
344
+ paddingHorizontal: spacing.lg,
345
+ paddingVertical: spacing.sm,
346
+ borderRadius: radius.pill,
347
+ },
348
+ submitDisabled: {
349
+ opacity: 0.5,
350
+ },
351
+ submitText: {
352
+ fontSize: fontSize.meta,
353
+ fontWeight: '600',
354
+ },
355
+ });
@@ -45,6 +45,7 @@ import { AttachmentChips } from '../../components/AttachmentChips';
45
45
  import { AttachmentMenu, showNativeAttachmentMenu } from '../../components/AttachmentMenu';
46
46
  import { Avatar } from '../../components/Avatar';
47
47
  import { ImageViewer } from '../../components/ImageViewer';
48
+ import { getRatingRequest, RatingCard } from '../../components/RatingCard';
48
49
  import { WidgetErrorBoundary } from '../../components/ErrorBoundary';
49
50
  import { GradientBackground } from '../../components/Gradient';
50
51
  import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } from '../../components/icons';
@@ -82,6 +83,8 @@ export function ChatView({
82
83
  onPickAttachments,
83
84
  onSendMessage,
84
85
  onIdentifyVisitor,
86
+ onRateConversation,
87
+ defaultLocale,
85
88
  onLoadMore,
86
89
  onTyping,
87
90
  onBack,
@@ -489,6 +492,8 @@ export function ChatView({
489
492
  color={color}
490
493
  labels={labels}
491
494
  onOpenImage={setViewerImage}
495
+ onRateConversation={onRateConversation}
496
+ locales={[locale, defaultLocale]}
492
497
  senderLabel={
493
498
  isAgentMessage && (startsRun || startsDay)
494
499
  ? message.senderName || agentName || labels.team
@@ -640,6 +645,8 @@ function MessageBubble({
640
645
  onSubmitProcedureForm,
641
646
  onAgentUiAction,
642
647
  onOpenImage,
648
+ onRateConversation,
649
+ locales,
643
650
  }: {
644
651
  message: Message;
645
652
  color: string;
@@ -650,6 +657,8 @@ function MessageBubble({
650
657
  onSubmitProcedureForm?: (body: string) => void;
651
658
  onAgentUiAction?: (action: AgentUiAction) => void | Promise<void>;
652
659
  onOpenImage: (image: { url: string; name: string }) => void;
660
+ onRateConversation?: ChatViewProps['onRateConversation'];
661
+ locales: Array<string | null | undefined>;
653
662
  }) {
654
663
  const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
655
664
  if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
@@ -668,6 +677,23 @@ function MessageBubble({
668
677
  );
669
678
  }
670
679
 
680
+ const ratingRequest = message.messageType === 'system' ? getRatingRequest(message.metadata) : null;
681
+ if (ratingRequest) {
682
+ return (
683
+ <RatingCard
684
+ ratingId={ratingRequest.ratingId}
685
+ initialRating={ratingRequest.rating}
686
+ initialComment={ratingRequest.comment}
687
+ initialReason={ratingRequest.reason}
688
+ reasons={ratingRequest.reasons}
689
+ locales={locales}
690
+ color={color}
691
+ labels={labels.rating}
692
+ onRate={onRateConversation ? (ratingId, data) => onRateConversation(message.id, ratingId, data) : undefined}
693
+ />
694
+ );
695
+ }
696
+
671
697
  if (message.messageType === 'system') {
672
698
  const text = message.bodyPlain || stripHtml(message.body || '') || labels.conversationUpdated;
673
699
  return (