@repzo/widget-react-native 1.0.5 → 1.0.7

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 +15 -0
  4. package/lib/commonjs/i18n.js.map +1 -1
  5. package/lib/commonjs/locales.js +14 -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 +14 -0
  12. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  13. package/lib/commonjs/ui/components/RatingCard.js +351 -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 +15 -0
  20. package/lib/module/i18n.js.map +1 -1
  21. package/lib/module/locales.js +14 -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 +14 -0
  28. package/lib/module/ui/WidgetProvider.js.map +1 -1
  29. package/lib/module/ui/components/RatingCard.js +345 -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 +14 -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 +59 -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 +14 -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 +59 -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 +15 -0
  68. package/src/locales.ts +14 -0
  69. package/src/state/widget-store.ts +34 -0
  70. package/src/types.ts +19 -0
  71. package/src/ui/WidgetProvider.tsx +14 -0
  72. package/src/ui/components/RatingCard.tsx +362 -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.7",
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,21 @@ 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
+ ratingDone: 'Done',
127
+ ratingThanks: 'Thanks for your feedback!',
128
+ ratingLocked: 'This rating can no longer be changed',
129
+ ratingExpired: 'This conversation can no longer be rated',
130
+ ratingFailed: "Couldn't save your rating. Please try again.",
131
+ rating1: 'Terrible',
132
+ rating2: 'Bad',
133
+ rating3: 'OK',
134
+ rating4: 'Great',
135
+ rating5: 'Amazing',
121
136
  // Forms.
122
137
  procedureSend: 'Send details',
123
138
  procedureSent: 'Sent',
package/src/locales.ts CHANGED
@@ -74,6 +74,20 @@ const ar: LabelMap = {
74
74
  messageSeen: 'تمت المشاهدة',
75
75
  waitingForTeammate: 'بانتظار أحد أعضاء الفريق',
76
76
  joinedConversation: 'انضم إلى المحادثة',
77
+ ratingQuestion: 'كيف تقيّم هذه المحادثة؟',
78
+ ratingReasonPrompt: 'ما الذي لم يكن جيداً؟',
79
+ ratingCommentPlaceholder: 'أخبرنا المزيد (اختياري)',
80
+ ratingSubmit: 'إرسال',
81
+ ratingDone: 'تم',
82
+ ratingThanks: 'شكراً على ملاحظاتك!',
83
+ ratingLocked: 'لم يعد بالإمكان تغيير هذا التقييم',
84
+ ratingExpired: 'لم يعد بالإمكان تقييم هذه المحادثة',
85
+ ratingFailed: 'تعذّر حفظ تقييمك. حاول مرة أخرى.',
86
+ rating1: 'سيئ جداً',
87
+ rating2: 'سيئ',
88
+ rating3: 'مقبول',
89
+ rating4: 'جيد',
90
+ rating5: 'ممتاز',
77
91
  procedureSend: 'إرسال التفاصيل',
78
92
  procedureSent: 'تم الإرسال',
79
93
  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,18 @@ 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
+ 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
+ },
323
336
  attach: attachLabels,
324
337
  viewer: {
325
338
  close: labels.close,
@@ -331,6 +344,7 @@ export function RepzoWidgetProvider({
331
344
  onPickAttachments={pickAttachments}
332
345
  onSendMessage={handleSendMessage}
333
346
  onIdentifyVisitor={(data) => store.identifyVisitor(data)}
347
+ onRateConversation={(messageId, ratingId, data) => store.rateConversation(messageId, ratingId, data)}
334
348
  onLoadMore={() => store.loadMoreMessages()}
335
349
  onTyping={(isTyping) => store.notifyTyping(isTyping)}
336
350
  onBack={handleBack}
@@ -0,0 +1,362 @@
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
+ /** The button with nothing typed: the comment is optional. */
32
+ done: string;
33
+ thanks: string;
34
+ locked: string;
35
+ expired: string;
36
+ failed: string;
37
+ /** Accessibility names for the faces, worst to best. */
38
+ scale: [string, string, string, string, string];
39
+ }
40
+
41
+ /** 1–2 asks for a reason; 3 is neutral, 4–5 good. */
42
+ export const isBadRating = (rating: number | null) => rating != null && rating <= 2;
43
+
44
+ const isRatingReason = (value: unknown): value is RatingReason =>
45
+ !!value &&
46
+ typeof value === 'object' &&
47
+ typeof (value as RatingReason).id === 'string' &&
48
+ !!(value as RatingReason).labels &&
49
+ typeof (value as RatingReason).labels === 'object';
50
+
51
+ /** The rating card carried by a system message's metadata, or null. */
52
+ export function getRatingRequest(metadata: Record<string, unknown> | null | undefined) {
53
+ if (!metadata || metadata.event !== 'rating_request' || typeof metadata.ratingId !== 'string') return null;
54
+ const rating = typeof metadata.rating === 'number' && metadata.rating >= 1 && metadata.rating <= 5 ? metadata.rating : null;
55
+ const comment = typeof metadata.ratingComment === 'string' && metadata.ratingComment ? metadata.ratingComment : null;
56
+ const reason = typeof metadata.ratingReason === 'string' && metadata.ratingReason ? metadata.ratingReason : null;
57
+ const reasons = Array.isArray(metadata.ratingReasons) ? metadata.ratingReasons.filter(isRatingReason) : [];
58
+ return { ratingId: metadata.ratingId, rating, comment, reason, reasons };
59
+ }
60
+
61
+ /**
62
+ * A reason's label in the first of `locales` it has (full code, then its base
63
+ * language: "ar-SA" → "ar"), else English, else whatever it has.
64
+ */
65
+ export function reasonLabel(reason: RatingReason, locales: Array<string | null | undefined>): string {
66
+ for (const locale of [...locales, 'en']) {
67
+ if (!locale) continue;
68
+ const label = reason.labels[locale] || reason.labels[locale.split('-')[0]!];
69
+ if (label) return label;
70
+ }
71
+ return Object.values(reason.labels).find(Boolean) ?? reason.id;
72
+ }
73
+
74
+ interface RatingCardProps {
75
+ ratingId: string;
76
+ initialRating: number | null;
77
+ initialComment: string | null;
78
+ initialReason: string | null;
79
+ /** Offered after a bad rating (1–2). */
80
+ reasons: RatingReason[];
81
+ /** The visitor's language first, then the widget's default. */
82
+ locales: Array<string | null | undefined>;
83
+ color: string;
84
+ labels: RatingCardLabels;
85
+ onRate?: (
86
+ ratingId: string,
87
+ data: { rating: number; reason?: string; comment?: string }
88
+ ) => Promise<ConversationRatingResult>;
89
+ }
90
+
91
+ export function RatingCard({
92
+ ratingId,
93
+ initialRating,
94
+ initialComment,
95
+ initialReason,
96
+ reasons,
97
+ locales,
98
+ color,
99
+ labels,
100
+ onRate,
101
+ }: RatingCardProps) {
102
+ const [rating, setRating] = useState<number | null>(initialRating);
103
+ const [reason, setReason] = useState<string | null>(initialReason);
104
+ const [comment, setComment] = useState<string | null>(initialComment);
105
+ const [draft, setDraft] = useState('');
106
+ // The comment is optional: "Done" with nothing typed just closes the box —
107
+ // the face (and reason) are already saved.
108
+ const [finished, setFinished] = useState(false);
109
+ const [pending, setPending] = useState(false);
110
+ const [closedReason, setClosedReason] = useState<'locked' | 'expired' | null>(initialComment ? 'locked' : null);
111
+ const [failed, setFailed] = useState(false);
112
+ const interactive = !!onRate && !closedReason && !pending;
113
+ const hasDraft = draft.trim().length > 0;
114
+ const canSubmit = !pending;
115
+
116
+ const submit = async (next: { rating: number; reason?: string; comment?: string }) => {
117
+ if (!onRate || pending) return;
118
+ setPending(true);
119
+ setFailed(false);
120
+ try {
121
+ const result = await onRate(ratingId, next);
122
+ setRating(result.rating);
123
+ setReason(result.reason ?? null);
124
+ setComment(result.comment);
125
+ if (result.locked) setClosedReason('locked');
126
+ setDraft('');
127
+ } catch (error) {
128
+ const status = (error as { status?: number }).status;
129
+ if (status === 409) setClosedReason('locked');
130
+ else if (status === 410) setClosedReason('expired');
131
+ else setFailed(true);
132
+ } finally {
133
+ setPending(false);
134
+ }
135
+ };
136
+
137
+ const status =
138
+ closedReason === 'expired'
139
+ ? labels.expired
140
+ : closedReason === 'locked' && !comment
141
+ ? labels.locked
142
+ : failed
143
+ ? labels.failed
144
+ : rating != null
145
+ ? labels.thanks
146
+ : null;
147
+
148
+ return (
149
+ <View style={styles.row}>
150
+ <View style={styles.card} testID="repzo-rating-card">
151
+ <Text style={styles.question}>{labels.question}</Text>
152
+ <View style={styles.faces} accessibilityRole="radiogroup">
153
+ {RATING_SCALE.map((entry, index) => {
154
+ const selected = rating === entry.value;
155
+ return (
156
+ <Pressable
157
+ key={entry.value}
158
+ accessibilityRole="radio"
159
+ accessibilityLabel={labels.scale[index]}
160
+ accessibilityState={{ checked: selected, disabled: !interactive }}
161
+ disabled={!interactive}
162
+ hitSlop={4}
163
+ onPress={() => {
164
+ if (!selected) void submit({ rating: entry.value });
165
+ }}
166
+ style={({ pressed }) => [
167
+ styles.face,
168
+ selected && { borderColor: color, backgroundColor: withAlpha(color, 0.08) },
169
+ rating != null && !selected && styles.faceDimmed,
170
+ pressed && styles.facePressed,
171
+ ]}>
172
+ <Text style={styles.emoji}>{entry.emoji}</Text>
173
+ </Pressable>
174
+ );
175
+ })}
176
+ </View>
177
+
178
+ {status ? <Text style={[styles.status, failed && styles.statusFailed]}>{status}</Text> : null}
179
+
180
+ {isBadRating(rating) && reasons.length > 0 && (interactive || reason) ? (
181
+ <View style={styles.reasons}>
182
+ {interactive ? <Text style={styles.reasonPrompt}>{labels.reasonPrompt}</Text> : null}
183
+ <View style={styles.chips} accessibilityRole="radiogroup" accessibilityLabel={labels.reasonPrompt}>
184
+ {reasons
185
+ .filter((item) => interactive || item.id === reason)
186
+ .map((item) => {
187
+ const selected = item.id === reason;
188
+ return (
189
+ <Pressable
190
+ key={item.id}
191
+ accessibilityRole="radio"
192
+ accessibilityState={{ checked: selected, disabled: !interactive }}
193
+ disabled={!interactive}
194
+ onPress={() => {
195
+ if (!selected && rating != null) void submit({ rating, reason: item.id });
196
+ }}
197
+ style={({ pressed }) => [
198
+ styles.chip,
199
+ selected && { backgroundColor: color, borderColor: color },
200
+ pressed && styles.facePressed,
201
+ ]}>
202
+ <Text style={[styles.chipText, selected && { color: contrastingTextColor(color) }]}>
203
+ {reasonLabel(item, locales)}
204
+ </Text>
205
+ </Pressable>
206
+ );
207
+ })}
208
+ </View>
209
+ </View>
210
+ ) : null}
211
+
212
+ {comment ? (
213
+ <Text style={styles.comment}>{comment}</Text>
214
+ ) : rating != null && !closedReason && !finished ? (
215
+ <View style={styles.form}>
216
+ <TextInput
217
+ value={draft}
218
+ onChangeText={setDraft}
219
+ editable={!pending}
220
+ multiline
221
+ maxLength={2000}
222
+ placeholder={labels.commentPlaceholder}
223
+ placeholderTextColor={palette.textSubtle}
224
+ style={styles.input}
225
+ />
226
+ <Pressable
227
+ accessibilityRole="button"
228
+ disabled={!canSubmit}
229
+ onPress={() => {
230
+ const text = draft.trim();
231
+ if (text && rating != null) void submit({ rating, comment: text });
232
+ else setFinished(true);
233
+ }}
234
+ style={({ pressed }) => [
235
+ styles.submit,
236
+ { backgroundColor: color },
237
+ !canSubmit && styles.submitDisabled,
238
+ pressed && canSubmit && styles.facePressed,
239
+ ]}>
240
+ <Text style={[styles.submitText, { color: contrastingTextColor(color) }]}>{hasDraft ? labels.submit : labels.done}</Text>
241
+ </Pressable>
242
+ </View>
243
+ ) : null}
244
+ </View>
245
+ </View>
246
+ );
247
+ }
248
+
249
+ const styles = StyleSheet.create({
250
+ row: {
251
+ alignItems: 'center',
252
+ paddingVertical: spacing.md,
253
+ },
254
+ card: {
255
+ width: '100%',
256
+ maxWidth: 340,
257
+ gap: spacing.md,
258
+ padding: spacing.lg,
259
+ borderRadius: radius.xl,
260
+ borderWidth: StyleSheet.hairlineWidth,
261
+ borderColor: palette.border,
262
+ backgroundColor: palette.white,
263
+ },
264
+ question: {
265
+ textAlign: 'center',
266
+ color: palette.ink,
267
+ fontSize: fontSize.body,
268
+ fontWeight: '600',
269
+ },
270
+ faces: {
271
+ flexDirection: 'row',
272
+ justifyContent: 'center',
273
+ gap: spacing.sm,
274
+ },
275
+ face: {
276
+ width: 48,
277
+ height: 48,
278
+ borderRadius: 24,
279
+ borderWidth: 2,
280
+ borderColor: 'transparent',
281
+ alignItems: 'center',
282
+ justifyContent: 'center',
283
+ },
284
+ faceDimmed: {
285
+ opacity: 0.45,
286
+ },
287
+ facePressed: {
288
+ opacity: 0.7,
289
+ },
290
+ emoji: {
291
+ fontSize: 28,
292
+ },
293
+ status: {
294
+ textAlign: 'center',
295
+ color: palette.textMuted,
296
+ fontSize: fontSize.meta,
297
+ },
298
+ statusFailed: {
299
+ color: palette.danger,
300
+ },
301
+ reasons: {
302
+ gap: spacing.sm,
303
+ },
304
+ reasonPrompt: {
305
+ color: palette.ink,
306
+ fontSize: fontSize.meta,
307
+ fontWeight: '600',
308
+ },
309
+ chips: {
310
+ flexDirection: 'row',
311
+ flexWrap: 'wrap',
312
+ gap: spacing.sm,
313
+ },
314
+ chip: {
315
+ paddingHorizontal: spacing.md,
316
+ paddingVertical: spacing.sm,
317
+ borderRadius: radius.pill,
318
+ borderWidth: 1,
319
+ borderColor: palette.border,
320
+ backgroundColor: palette.white,
321
+ },
322
+ chipText: {
323
+ color: palette.ink,
324
+ fontSize: fontSize.meta,
325
+ },
326
+ comment: {
327
+ paddingHorizontal: spacing.md,
328
+ paddingVertical: spacing.sm,
329
+ borderRadius: radius.md,
330
+ backgroundColor: palette.surfaceAlt,
331
+ color: palette.ink,
332
+ fontSize: fontSize.meta,
333
+ writingDirection: 'auto',
334
+ },
335
+ form: {
336
+ gap: spacing.sm,
337
+ },
338
+ input: {
339
+ minHeight: 64,
340
+ paddingHorizontal: spacing.md,
341
+ paddingVertical: spacing.sm,
342
+ borderRadius: radius.md,
343
+ borderWidth: 1,
344
+ borderColor: palette.border,
345
+ color: palette.ink,
346
+ fontSize: fontSize.input,
347
+ textAlignVertical: 'top',
348
+ },
349
+ submit: {
350
+ alignSelf: 'flex-end',
351
+ paddingHorizontal: spacing.lg,
352
+ paddingVertical: spacing.sm,
353
+ borderRadius: radius.pill,
354
+ },
355
+ submitDisabled: {
356
+ opacity: 0.5,
357
+ },
358
+ submitText: {
359
+ fontSize: fontSize.meta,
360
+ fontWeight: '600',
361
+ },
362
+ });