@repzo/widget-react-native 1.0.9 → 1.1.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.
- package/CHANGELOG.md +365 -0
- package/README.md +55 -4
- package/lib/commonjs/agent-ui-blocks.js +2 -1
- package/lib/commonjs/agent-ui-blocks.js.map +1 -1
- package/lib/commonjs/attachments/pickers.js +3 -6
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/device-id.js +31 -3
- package/lib/commonjs/device-id.js.map +1 -1
- package/lib/commonjs/i18n.js +26 -4
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +21 -1
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/push/push-notifications.js +0 -1
- package/lib/commonjs/push/push-notifications.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +57 -20
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/storage.js +13 -1
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/commonjs/types.js +6 -3
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +74 -47
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AgentUiBlocks.js +35 -15
- package/lib/commonjs/ui/components/AgentUiBlocks.js.map +1 -1
- package/lib/commonjs/ui/components/Launcher.js +4 -1
- package/lib/commonjs/ui/components/Launcher.js.map +1 -1
- package/lib/commonjs/ui/components/Markdown.js +32 -2
- package/lib/commonjs/ui/components/Markdown.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +6 -3
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-widget-insets.js +0 -1
- package/lib/commonjs/ui/hooks/use-widget-insets.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +67 -29
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/utils/html.js +8 -1
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/native-gradient.js +0 -1
- package/lib/commonjs/ui/utils/native-gradient.js.map +1 -1
- package/lib/module/agent-ui-blocks.js +2 -1
- package/lib/module/agent-ui-blocks.js.map +1 -1
- package/lib/module/attachments/pickers.js +3 -6
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/device-id.js +31 -3
- package/lib/module/device-id.js.map +1 -1
- package/lib/module/i18n.js +26 -4
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +21 -1
- package/lib/module/locales.js.map +1 -1
- package/lib/module/push/push-notifications.js +0 -1
- package/lib/module/push/push-notifications.js.map +1 -1
- package/lib/module/state/widget-store.js +58 -20
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/storage.js +13 -1
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js +6 -3
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +74 -47
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AgentUiBlocks.js +35 -15
- package/lib/module/ui/components/AgentUiBlocks.js.map +1 -1
- package/lib/module/ui/components/Launcher.js +4 -1
- package/lib/module/ui/components/Launcher.js.map +1 -1
- package/lib/module/ui/components/Markdown.js +32 -3
- package/lib/module/ui/components/Markdown.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +6 -3
- package/lib/module/ui/components/RatingCard.js.map +1 -1
- package/lib/module/ui/hooks/use-widget-insets.js +0 -1
- package/lib/module/ui/hooks/use-widget-insets.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +68 -30
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/utils/html.js +8 -1
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/native-gradient.js +0 -1
- package/lib/module/ui/utils/native-gradient.js.map +1 -1
- package/lib/typescript/commonjs/agent-ui-blocks.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/device-id.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +22 -1
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +13 -7
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +14 -8
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts +14 -0
- package/lib/typescript/commonjs/ui/components/AgentUiBlocks.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Markdown.d.ts +8 -0
- package/lib/typescript/commonjs/ui/components/Markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-widget-insets.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +10 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/native-gradient.d.ts.map +1 -1
- package/lib/typescript/module/agent-ui-blocks.d.ts.map +1 -1
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/device-id.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +22 -1
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +13 -7
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/storage.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +14 -8
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts +14 -0
- package/lib/typescript/module/ui/components/AgentUiBlocks.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Markdown.d.ts +8 -0
- package/lib/typescript/module/ui/components/Markdown.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-widget-insets.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +10 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/native-gradient.d.ts.map +1 -1
- package/package.json +14 -4
- package/src/agent-ui-blocks.ts +2 -1
- package/src/attachments/pickers.ts +3 -6
- package/src/device-id.ts +34 -3
- package/src/i18n.ts +25 -2
- package/src/locales.ts +20 -0
- package/src/push/push-notifications.ts +0 -1
- package/src/state/widget-store.ts +52 -19
- package/src/storage.ts +18 -1
- package/src/types.ts +14 -8
- package/src/ui/WidgetProvider.tsx +88 -46
- package/src/ui/components/AgentUiBlocks.tsx +40 -12
- package/src/ui/components/Launcher.tsx +6 -1
- package/src/ui/components/Markdown.tsx +34 -3
- package/src/ui/components/RatingCard.tsx +6 -2
- package/src/ui/hooks/use-widget-insets.ts +0 -1
- package/src/ui/screens/chat/ChatView.tsx +77 -42
- package/src/ui/screens/chat/ChatView.types.ts +10 -0
- package/src/ui/utils/html.ts +9 -1
- package/src/ui/utils/native-gradient.ts +0 -1
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
|
-
/**
|
|
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;
|
|
@@ -490,13 +496,6 @@ export interface RepzoWidgetStorageAdapter {
|
|
|
490
496
|
removeItem(key: string): Promise<void> | void;
|
|
491
497
|
}
|
|
492
498
|
|
|
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
499
|
/** A bad-rating reason on the rating card, labelled per language. */
|
|
501
500
|
export interface RatingReason {
|
|
502
501
|
id: string;
|
|
@@ -516,6 +515,13 @@ export interface ConversationRatingResult {
|
|
|
516
515
|
locked: boolean;
|
|
517
516
|
}
|
|
518
517
|
|
|
518
|
+
/**
|
|
519
|
+
* `RepzoWidgetError.code`. The last two come from the server's 403 body and
|
|
520
|
+
* are terminal — the SDK stops realtime and does not retry on its own:
|
|
521
|
+
* - `messenger_api_disabled` — the channel's Messenger API is switched off.
|
|
522
|
+
* - `identity_verification_failed` — `userHash` missing/wrong while the
|
|
523
|
+
* channel enforces identity verification.
|
|
524
|
+
*/
|
|
519
525
|
export type RepzoWidgetErrorCode =
|
|
520
526
|
| 'network'
|
|
521
527
|
| 'http'
|
|
@@ -39,6 +39,7 @@ import { useKeyboardOverlap } from './hooks/use-keyboard-overlap';
|
|
|
39
39
|
import { TopInsetContext } from './hooks/use-top-inset';
|
|
40
40
|
import { useWidgetInsets } from './hooks/use-widget-insets';
|
|
41
41
|
import { ChatView } from './screens/chat/ChatView';
|
|
42
|
+
import type { ChatLabels } from './screens/chat/ChatView.types';
|
|
42
43
|
import { ConversationsList } from './screens/conversations/ConversationsList';
|
|
43
44
|
import { HomeView, resolveHeaderTextColor } from './screens/home/HomeView';
|
|
44
45
|
import { PreChatForm } from './screens/pre-chat/PreChatForm';
|
|
@@ -135,6 +136,17 @@ export function RepzoWidgetProvider({
|
|
|
135
136
|
professional: labels.blockProfessional,
|
|
136
137
|
priceOffer: labels.blockPriceOffer,
|
|
137
138
|
working: labels.working,
|
|
139
|
+
subtotal: labels.blockSubtotal,
|
|
140
|
+
discount: labels.blockDiscount,
|
|
141
|
+
tax: labels.blockTax,
|
|
142
|
+
shipping: labels.blockShipping,
|
|
143
|
+
total: labels.blockTotal,
|
|
144
|
+
minutes: labels.blockMinutes,
|
|
145
|
+
save: labels.blockSave,
|
|
146
|
+
anyAvailable: labels.blockAnyAvailable,
|
|
147
|
+
withProvider: labels.blockWithProvider,
|
|
148
|
+
bookTime: labels.blockBookTime,
|
|
149
|
+
validUntil: labels.blockValidUntil,
|
|
138
150
|
}),
|
|
139
151
|
[labels]
|
|
140
152
|
);
|
|
@@ -156,6 +168,62 @@ export function RepzoWidgetProvider({
|
|
|
156
168
|
[labels]
|
|
157
169
|
);
|
|
158
170
|
|
|
171
|
+
// Stable across renders so each message bubble only re-renders when its
|
|
172
|
+
// own message changes, not on every store update or keystroke.
|
|
173
|
+
const chatLabels = useMemo<ChatLabels>(
|
|
174
|
+
() => ({
|
|
175
|
+
back: labels.back,
|
|
176
|
+
loading: labels.loading,
|
|
177
|
+
loadEarlier: labels.loadEarlier,
|
|
178
|
+
emptyConversation: labels.emptyConversation,
|
|
179
|
+
messagePlaceholder: labels.messagePlaceholder,
|
|
180
|
+
failedToSend: labels.failedToSend,
|
|
181
|
+
thinking: labels.thinking,
|
|
182
|
+
team: labels.team,
|
|
183
|
+
statusOnline: labels.homeStatusOnline,
|
|
184
|
+
statusAway: labels.homeStatusAway,
|
|
185
|
+
close: labels.close,
|
|
186
|
+
today: labels.today,
|
|
187
|
+
yesterday: labels.yesterday,
|
|
188
|
+
conversationUpdated: labels.conversationUpdated,
|
|
189
|
+
messageDeleted: labels.messageDeleted,
|
|
190
|
+
messageSent: labels.messageSent,
|
|
191
|
+
messageSeen: labels.messageSeen,
|
|
192
|
+
waitingForTeammate: labels.waitingForTeammate,
|
|
193
|
+
joinedConversation: labels.joinedConversation,
|
|
194
|
+
procedureSend: labels.procedureSend,
|
|
195
|
+
procedureSent: labels.procedureSent,
|
|
196
|
+
procedureShareDetails: labels.procedureShareDetails,
|
|
197
|
+
procedureDetailsSent: labels.procedureDetailsSent,
|
|
198
|
+
procedureHide: labels.procedureHide,
|
|
199
|
+
procedureFieldCount: labels.procedureFieldCount,
|
|
200
|
+
procedureFieldCountOne: labels.procedureFieldCountOne,
|
|
201
|
+
procedureTitle: labels.procedureTitle,
|
|
202
|
+
procedureEmailPlaceholder: labels.procedureEmailPlaceholder,
|
|
203
|
+
procedurePhonePlaceholder: labels.procedurePhonePlaceholder,
|
|
204
|
+
blocks: blockLabels,
|
|
205
|
+
rating: {
|
|
206
|
+
question: labels.ratingQuestion,
|
|
207
|
+
reasonPrompt: labels.ratingReasonPrompt,
|
|
208
|
+
commentPlaceholder: labels.ratingCommentPlaceholder,
|
|
209
|
+
submit: labels.ratingSubmit,
|
|
210
|
+
done: labels.ratingDone,
|
|
211
|
+
thanks: labels.ratingThanks,
|
|
212
|
+
locked: labels.ratingLocked,
|
|
213
|
+
expired: labels.ratingExpired,
|
|
214
|
+
failed: labels.ratingFailed,
|
|
215
|
+
scale: [labels.rating1, labels.rating2, labels.rating3, labels.rating4, labels.rating5],
|
|
216
|
+
},
|
|
217
|
+
attach: attachLabels,
|
|
218
|
+
viewer: {
|
|
219
|
+
close: labels.close,
|
|
220
|
+
loadFailed: labels.imageLoadFailed,
|
|
221
|
+
openExternally: labels.openImageExternally,
|
|
222
|
+
},
|
|
223
|
+
}),
|
|
224
|
+
[labels, blockLabels, attachLabels]
|
|
225
|
+
);
|
|
226
|
+
|
|
159
227
|
// Read per render: `boot({ attachments })` sets it outside React state.
|
|
160
228
|
const attachmentSources = theme.attachmentsEnabled ? getAttachmentSources() : [];
|
|
161
229
|
|
|
@@ -176,6 +244,21 @@ export function RepzoWidgetProvider({
|
|
|
176
244
|
[store]
|
|
177
245
|
);
|
|
178
246
|
|
|
247
|
+
const handleIdentifyVisitor = useCallback(
|
|
248
|
+
(data: { email?: string; name?: string; phone?: string }) => store.identifyVisitor(data),
|
|
249
|
+
[store]
|
|
250
|
+
);
|
|
251
|
+
const handleRateConversation = useCallback(
|
|
252
|
+
(
|
|
253
|
+
messageId: string,
|
|
254
|
+
ratingId: string,
|
|
255
|
+
data: { rating: number; reason?: string; comment?: string }
|
|
256
|
+
) => store.rateConversation(messageId, ratingId, data),
|
|
257
|
+
[store]
|
|
258
|
+
);
|
|
259
|
+
const handleLoadMore = useCallback(() => void store.loadMoreMessages(), [store]);
|
|
260
|
+
const handleTyping = useCallback((isTyping: boolean) => store.notifyTyping(isTyping), [store]);
|
|
261
|
+
|
|
179
262
|
const handlePreChatSubmit = useCallback(
|
|
180
263
|
async (data: Record<string, string>) => {
|
|
181
264
|
try {
|
|
@@ -298,55 +381,14 @@ export function RepzoWidgetProvider({
|
|
|
298
381
|
defaultLocale={state.config?.defaultLocale}
|
|
299
382
|
prefillText={state.prefillText}
|
|
300
383
|
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
|
-
}}
|
|
384
|
+
labels={chatLabels}
|
|
343
385
|
attachmentSources={attachmentSources}
|
|
344
386
|
onPickAttachments={pickAttachments}
|
|
345
387
|
onSendMessage={handleSendMessage}
|
|
346
|
-
onIdentifyVisitor={
|
|
347
|
-
onRateConversation={
|
|
348
|
-
onLoadMore={
|
|
349
|
-
onTyping={
|
|
388
|
+
onIdentifyVisitor={handleIdentifyVisitor}
|
|
389
|
+
onRateConversation={handleRateConversation}
|
|
390
|
+
onLoadMore={handleLoadMore}
|
|
391
|
+
onTyping={handleTyping}
|
|
350
392
|
onBack={handleBack}
|
|
351
393
|
onClose={handleClose}
|
|
352
394
|
/>
|
|
@@ -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
|
-
?
|
|
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=
|
|
211
|
+
<MoneyRow label={labels.subtotal} value={formatMoney(block.subtotal, block.currency)} />
|
|
186
212
|
{block.discount ? (
|
|
187
213
|
<MoneyRow
|
|
188
|
-
label=
|
|
214
|
+
label={labels.discount}
|
|
189
215
|
value={`−${formatMoney(block.discount, block.currency)}`}
|
|
190
216
|
/>
|
|
191
217
|
) : null}
|
|
192
218
|
{block.tax ? (
|
|
193
|
-
<MoneyRow label=
|
|
219
|
+
<MoneyRow label={labels.tax} value={formatMoney(block.tax, block.currency)} />
|
|
194
220
|
) : null}
|
|
195
221
|
{block.shipping ? (
|
|
196
222
|
<MoneyRow
|
|
197
|
-
label=
|
|
223
|
+
label={labels.shipping}
|
|
198
224
|
value={formatMoney(block.shipping, block.currency)}
|
|
199
225
|
/>
|
|
200
226
|
) : null}
|
|
201
|
-
<MoneyRow label=
|
|
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
|
-
|
|
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}
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
63
|
+
accessibilityLabel={accessibilityLabel}
|
|
59
64
|
style={({ pressed }) => [
|
|
60
65
|
styles.bubble,
|
|
61
66
|
showText ? styles.bubbleWithText : styles.bubbleRound,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* (including bare URLs, linkified the same way the web widget does).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { Fragment } from 'react';
|
|
10
|
+
import { Fragment, useMemo } from 'react';
|
|
11
11
|
import { StyleSheet, Text, View } from 'react-native';
|
|
12
12
|
|
|
13
13
|
import { fontSize, palette, radius, spacing } from '../styles/theme';
|
|
@@ -58,6 +58,36 @@ function linkifyPlainText(text: string): string {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** Host of an absolute web URL, lower-cased and without `www.`; null otherwise. */
|
|
62
|
+
function targetHost(href: string): string | null {
|
|
63
|
+
const match = /^https?:\/\/(?:[^@/?#\s]*@)?([^/?#:\s]+)/i.exec(href.trim());
|
|
64
|
+
return match ? match[1]!.toLowerCase().replace(/^www\./, '') : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ADDRESS_LABEL =
|
|
68
|
+
/^(?:https?:\/\/)?(?:www\.)?((?:[a-z0-9-]+\.)+[a-z]{2,24})(?::\d+)?(?:[/?#]\S*)?$/i;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The text a link shows. Agent and AI replies are remote content, and
|
|
72
|
+
* `[https://yourbank.com](https://evil.example)` would read as one site while
|
|
73
|
+
* opening another: when the label itself reads as a web address on a
|
|
74
|
+
* different host than the target, the real target is shown instead. Ordinary
|
|
75
|
+
* labels ("Book here", `report.pdf`) are left alone.
|
|
76
|
+
*/
|
|
77
|
+
export function linkLabel(label: string, href: string): string {
|
|
78
|
+
const text = label.trim();
|
|
79
|
+
if (!text) return href;
|
|
80
|
+
const looksLikeScheme = /^(?:[a-z][a-z0-9+.-]*:\/\/|www\.)/i.test(text);
|
|
81
|
+
const address = ADDRESS_LABEL.exec(text);
|
|
82
|
+
if (!address && !looksLikeScheme) return label;
|
|
83
|
+
if (address && !looksLikeScheme && !text.includes('/')) {
|
|
84
|
+
const extension = address[1]!.split('.').pop()?.toLowerCase() ?? '';
|
|
85
|
+
if (NON_LINK_EXTENSIONS.has(extension)) return label;
|
|
86
|
+
}
|
|
87
|
+
const shown = address ? address[1]!.toLowerCase().replace(/^www\./, '') : null;
|
|
88
|
+
return shown && shown === targetHost(href) ? label : href;
|
|
89
|
+
}
|
|
90
|
+
|
|
61
91
|
/** Turn bare URLs into markdown links while leaving code and existing links alone. */
|
|
62
92
|
export function linkifyMarkdown(text: string): string {
|
|
63
93
|
if (!text) return text;
|
|
@@ -108,8 +138,8 @@ function InlineMarkdown({ content, color }: { content: string; color: string })
|
|
|
108
138
|
|
|
109
139
|
const link = /^!?\[([^\]]*)\]\(([^)]*)\)$/.exec(part);
|
|
110
140
|
if (link) {
|
|
111
|
-
const label = link[1] || link[2] || '';
|
|
112
141
|
const href = link[2] || '';
|
|
142
|
+
const label = linkLabel(link[1] || '', href);
|
|
113
143
|
return (
|
|
114
144
|
<Text
|
|
115
145
|
key={key}
|
|
@@ -134,7 +164,8 @@ interface MarkdownProps {
|
|
|
134
164
|
}
|
|
135
165
|
|
|
136
166
|
export function Markdown({ content, color, linkColor }: MarkdownProps) {
|
|
137
|
-
|
|
167
|
+
// Parsed once per body: a thread re-renders far more often than it changes.
|
|
168
|
+
const blocks = useMemo(() => parseBlocks(linkifyMarkdown(content)), [content]);
|
|
138
169
|
const effectiveLinkColor = linkColor ?? color;
|
|
139
170
|
|
|
140
171
|
return (
|
|
@@ -104,8 +104,10 @@ export function RatingCard({
|
|
|
104
104
|
const [comment, setComment] = useState<string | null>(initialComment);
|
|
105
105
|
const [draft, setDraft] = useState('');
|
|
106
106
|
// The comment is optional: "Done" with nothing typed just closes the box —
|
|
107
|
-
// the face (and reason) are already saved.
|
|
108
|
-
|
|
107
|
+
// the face (and reason) are already saved. A card that opens with a saved
|
|
108
|
+
// rating starts closed too, so "Done" sticks when the thread is reopened;
|
|
109
|
+
// tapping the chosen face opens the box again.
|
|
110
|
+
const [finished, setFinished] = useState(initialRating != null);
|
|
109
111
|
const [pending, setPending] = useState(false);
|
|
110
112
|
const [closedReason, setClosedReason] = useState<'locked' | 'expired' | null>(initialComment ? 'locked' : null);
|
|
111
113
|
const [failed, setFailed] = useState(false);
|
|
@@ -124,6 +126,7 @@ export function RatingCard({
|
|
|
124
126
|
setComment(result.comment);
|
|
125
127
|
if (result.locked) setClosedReason('locked');
|
|
126
128
|
setDraft('');
|
|
129
|
+
setFinished(false);
|
|
127
130
|
} catch (error) {
|
|
128
131
|
const status = (error as { status?: number }).status;
|
|
129
132
|
if (status === 409) setClosedReason('locked');
|
|
@@ -162,6 +165,7 @@ export function RatingCard({
|
|
|
162
165
|
hitSlop={4}
|
|
163
166
|
onPress={() => {
|
|
164
167
|
if (!selected) void submit({ rating: entry.value });
|
|
168
|
+
else setFinished(false);
|
|
165
169
|
}}
|
|
166
170
|
style={({ pressed }) => [
|
|
167
171
|
styles.face,
|
|
@@ -25,7 +25,6 @@ const FALLBACK_INSETS: WidgetInsets = {
|
|
|
25
25
|
|
|
26
26
|
let insetsHook: (() => WidgetInsets) | null = null;
|
|
27
27
|
try {
|
|
28
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
29
28
|
const mod = require('react-native-safe-area-context');
|
|
30
29
|
if (typeof mod?.useSafeAreaInsets === 'function') insetsHook = mod.useSafeAreaInsets;
|
|
31
30
|
} catch {
|