@repzo/widget-react-native 1.0.8 → 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 +56 -4
- package/lib/commonjs/agent-ui-blocks.js +2 -1
- package/lib/commonjs/agent-ui-blocks.js.map +1 -1
- package/lib/commonjs/api/client.js +3 -0
- package/lib/commonjs/api/client.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/api/client.js +3 -0
- package/lib/module/api/client.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/api/client.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 +15 -7
- 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/api/client.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 +15 -7
- 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/api/client.ts +6 -0
- 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 +15 -7
- 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/api/client.ts
CHANGED
|
@@ -215,6 +215,12 @@ export class WidgetApiClient {
|
|
|
215
215
|
...(params.user.userId ? { user_id: params.user.userId } : {}),
|
|
216
216
|
...(params.user.userHash ? { user_hash: params.user.userHash } : {}),
|
|
217
217
|
...(params.user.account?.id ? { account: params.user.account } : {}),
|
|
218
|
+
...(params.user.signedUpAt !== undefined && params.user.signedUpAt !== null
|
|
219
|
+
? {
|
|
220
|
+
signed_up_at:
|
|
221
|
+
params.user.signedUpAt instanceof Date ? params.user.signedUpAt.toISOString() : params.user.signedUpAt,
|
|
222
|
+
}
|
|
223
|
+
: {}),
|
|
218
224
|
}
|
|
219
225
|
: undefined;
|
|
220
226
|
|
|
@@ -63,12 +63,12 @@ export interface PickResult {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// ─── Host integrations ─────────────────────────────────────────────────────
|
|
66
|
-
// Resolved once at module load. Each `require` is static so Metro can see it
|
|
67
|
-
//
|
|
66
|
+
// Resolved once at module load. Each `require` is static so Metro can see it;
|
|
67
|
+
// with `allowOptionalDependencies` (see metro.js) an uninstalled one bundles as
|
|
68
|
+
// a require that throws, which the `try` around it catches.
|
|
68
69
|
|
|
69
70
|
let imagePicker: any = null;
|
|
70
71
|
try {
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
72
72
|
const mod = require('expo-image-picker');
|
|
73
73
|
if (typeof mod?.launchImageLibraryAsync === 'function') imagePicker = mod;
|
|
74
74
|
} catch {
|
|
@@ -77,7 +77,6 @@ try {
|
|
|
77
77
|
|
|
78
78
|
let documentPicker: any = null;
|
|
79
79
|
try {
|
|
80
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
81
80
|
const mod = require('expo-document-picker');
|
|
82
81
|
if (typeof mod?.getDocumentAsync === 'function') documentPicker = mod;
|
|
83
82
|
} catch {
|
|
@@ -86,7 +85,6 @@ try {
|
|
|
86
85
|
|
|
87
86
|
let fileSystem: any = null;
|
|
88
87
|
try {
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
90
88
|
fileSystem = require('expo-file-system') ?? null;
|
|
91
89
|
} catch {
|
|
92
90
|
fileSystem = null;
|
|
@@ -94,7 +92,6 @@ try {
|
|
|
94
92
|
|
|
95
93
|
let imageManipulator: any = null;
|
|
96
94
|
try {
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
98
95
|
const mod = require('expo-image-manipulator');
|
|
99
96
|
if (
|
|
100
97
|
typeof mod?.manipulateAsync === 'function' ||
|
package/src/device-id.ts
CHANGED
|
@@ -10,9 +10,33 @@ import { DEVICE_ID_KEY, Storage } from './storage';
|
|
|
10
10
|
let cached: string | null = null;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
13
|
+
* The host's `expo-crypto`, if installed — Hermes has no `crypto` global
|
|
14
|
+
* unless the app adds a polyfill. Loaded like every other optional peer.
|
|
15
|
+
*/
|
|
16
|
+
function expoCryptoBytes(bytes: Uint8Array): boolean {
|
|
17
|
+
try {
|
|
18
|
+
const mod = require('expo-crypto');
|
|
19
|
+
if (typeof mod?.getRandomValues === 'function') {
|
|
20
|
+
mod.getRandomValues(bytes);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (typeof mod?.getRandomBytes === 'function') {
|
|
24
|
+
const random: unknown = mod.getRandomBytes(bytes.length);
|
|
25
|
+
if (random instanceof Uint8Array && random.length === bytes.length) {
|
|
26
|
+
bytes.set(random);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Not installed, or its native module is missing (Expo Go, tests).
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 16 random bytes from `crypto.getRandomValues` (RN with a polyfill, newer
|
|
38
|
+
* Hermes), then `expo-crypto`; only without either, Math.random — fine for a
|
|
39
|
+
* correlation id but guessable, so development builds say so.
|
|
16
40
|
*/
|
|
17
41
|
function randomBytes(): Uint8Array {
|
|
18
42
|
const bytes = new Uint8Array(16);
|
|
@@ -26,6 +50,13 @@ function randomBytes(): Uint8Array {
|
|
|
26
50
|
// Present but unusable (some polyfills throw) — fall through.
|
|
27
51
|
}
|
|
28
52
|
}
|
|
53
|
+
if (expoCryptoBytes(bytes)) return bytes;
|
|
54
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
55
|
+
console.warn(
|
|
56
|
+
'[repzo-widget] no secure random source, so the device id uses Math.random. Install ' +
|
|
57
|
+
'expo-crypto or react-native-get-random-values — see "Requirements" in the README.'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
29
60
|
for (let index = 0; index < bytes.length; index += 1) {
|
|
30
61
|
bytes[index] = (Math.random() * 256) | 0;
|
|
31
62
|
}
|
package/src/i18n.ts
CHANGED
|
@@ -32,16 +32,17 @@ export function createTranslator(config: WidgetConfig | null, locale: string): T
|
|
|
32
32
|
/**
|
|
33
33
|
* Dashboard copy that's written per language: the default language's text is
|
|
34
34
|
* the plain config field and every other enabled language's lives under the
|
|
35
|
-
* same key in `translations`.
|
|
35
|
+
* same key in `translations`. The offline message is written the same way.
|
|
36
36
|
*/
|
|
37
37
|
const WELCOME_TEXT_KEYS = [
|
|
38
38
|
'welcomeHeading',
|
|
39
39
|
'welcomeSubheading',
|
|
40
40
|
'startConversationLabel',
|
|
41
41
|
'replyTimeLabel',
|
|
42
|
+
'offlineMessage',
|
|
42
43
|
] as const;
|
|
43
44
|
|
|
44
|
-
/** The config with its welcome text in the active locale. */
|
|
45
|
+
/** The config with its welcome text and offline message in the active locale. */
|
|
45
46
|
export function localizeWelcomeText(config: WidgetConfig | null, locale: string): WidgetConfig | null {
|
|
46
47
|
if (!config) return config;
|
|
47
48
|
const translate = createTranslator(config, locale);
|
|
@@ -136,6 +137,14 @@ export const DEFAULT_LABELS = {
|
|
|
136
137
|
// Forms.
|
|
137
138
|
procedureSend: 'Send details',
|
|
138
139
|
procedureSent: 'Sent',
|
|
140
|
+
procedureShareDetails: 'Share details',
|
|
141
|
+
procedureDetailsSent: 'Details sent',
|
|
142
|
+
procedureHide: 'Hide',
|
|
143
|
+
procedureFieldCount: '{count} fields',
|
|
144
|
+
procedureFieldCountOne: '1 field',
|
|
145
|
+
procedureTitle: 'Details',
|
|
146
|
+
procedureEmailPlaceholder: 'name@example.com',
|
|
147
|
+
procedurePhonePlaceholder: '+962 7...',
|
|
139
148
|
fieldRequired: '{field} is required',
|
|
140
149
|
invalidEmail: 'Enter a valid email address',
|
|
141
150
|
working: 'Working...',
|
|
@@ -154,6 +163,20 @@ export const DEFAULT_LABELS = {
|
|
|
154
163
|
blockChooseTime: 'Choose a time',
|
|
155
164
|
blockProfessional: 'Professional',
|
|
156
165
|
blockPriceOffer: 'Price offer',
|
|
166
|
+
blockSubtotal: 'Subtotal',
|
|
167
|
+
blockDiscount: 'Discount',
|
|
168
|
+
blockTax: 'Tax',
|
|
169
|
+
blockShipping: 'Shipping',
|
|
170
|
+
blockTotal: 'Total',
|
|
171
|
+
blockMinutes: '{count} min',
|
|
172
|
+
blockSave: 'Save',
|
|
173
|
+
blockAnyAvailable: 'Any available',
|
|
174
|
+
blockWithProvider: 'With {name}',
|
|
175
|
+
blockBookTime: 'Book this time',
|
|
176
|
+
blockValidUntil: 'Valid until {date}',
|
|
177
|
+
// Floating launcher button (screen reader names).
|
|
178
|
+
launcherOpen: 'Open chat',
|
|
179
|
+
launcherClose: 'Close chat',
|
|
157
180
|
} as const;
|
|
158
181
|
|
|
159
182
|
export type LabelKey = keyof typeof DEFAULT_LABELS;
|
package/src/locales.ts
CHANGED
|
@@ -90,6 +90,13 @@ const ar: LabelMap = {
|
|
|
90
90
|
rating5: 'ممتاز',
|
|
91
91
|
procedureSend: 'إرسال التفاصيل',
|
|
92
92
|
procedureSent: 'تم الإرسال',
|
|
93
|
+
procedureShareDetails: 'مشاركة التفاصيل',
|
|
94
|
+
procedureDetailsSent: 'تم إرسال التفاصيل',
|
|
95
|
+
procedureHide: 'إخفاء',
|
|
96
|
+
// "Fields: 5" sidesteps Arabic's count-dependent plural forms.
|
|
97
|
+
procedureFieldCount: 'الحقول: {count}',
|
|
98
|
+
procedureFieldCountOne: 'حقل واحد',
|
|
99
|
+
procedureTitle: 'التفاصيل',
|
|
93
100
|
fieldRequired: '{field} مطلوب',
|
|
94
101
|
invalidEmail: 'أدخل بريدًا إلكترونيًا صالحًا',
|
|
95
102
|
working: 'جارٍ العمل...',
|
|
@@ -105,6 +112,19 @@ const ar: LabelMap = {
|
|
|
105
112
|
blockChooseTime: 'اختر وقتًا',
|
|
106
113
|
blockProfessional: 'الموظف',
|
|
107
114
|
blockPriceOffer: 'عرض سعر',
|
|
115
|
+
blockSubtotal: 'المجموع الفرعي',
|
|
116
|
+
blockDiscount: 'الخصم',
|
|
117
|
+
blockTax: 'الضريبة',
|
|
118
|
+
blockShipping: 'الشحن',
|
|
119
|
+
blockTotal: 'الإجمالي',
|
|
120
|
+
blockMinutes: '{count} دقيقة',
|
|
121
|
+
blockSave: 'حفظ',
|
|
122
|
+
blockAnyAvailable: 'أي موظف متاح',
|
|
123
|
+
blockWithProvider: 'مع {name}',
|
|
124
|
+
blockBookTime: 'احجز هذا الموعد',
|
|
125
|
+
blockValidUntil: 'صالح حتى {date}',
|
|
126
|
+
launcherOpen: 'فتح المحادثة',
|
|
127
|
+
launcherClose: 'إغلاق المحادثة',
|
|
108
128
|
};
|
|
109
129
|
|
|
110
130
|
export const BUNDLED_TRANSLATIONS: Record<string, LabelMap> = { ar };
|
|
@@ -50,7 +50,6 @@ interface MessagingHandle {
|
|
|
50
50
|
|
|
51
51
|
function resolveMessaging(): MessagingHandle | null {
|
|
52
52
|
try {
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
54
53
|
const mod = require('@react-native-firebase/messaging');
|
|
55
54
|
|
|
56
55
|
// Modular API — the only one in RNFB v26+ (the namespaced default export
|
|
@@ -36,7 +36,8 @@ import type {
|
|
|
36
36
|
} from '../types';
|
|
37
37
|
import { setOpenUrlHandler } from '../ui/utils/safe-url';
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
40
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.1.0';
|
|
40
41
|
|
|
41
42
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
42
43
|
function sessionPlatform(): 'ios' | 'android' | 'web' {
|
|
@@ -755,6 +756,7 @@ export class WidgetStore {
|
|
|
755
756
|
this.userKeyLoaded = true;
|
|
756
757
|
this.pendingAgentByConversation = {};
|
|
757
758
|
this.seenMessageIds = [];
|
|
759
|
+
this.earlyStatuses.clear();
|
|
758
760
|
this.visitorTypingActive = false;
|
|
759
761
|
this.lastTypingPingAt = 0;
|
|
760
762
|
this.launchCardOffered = false;
|
|
@@ -1071,12 +1073,6 @@ export class WidgetStore {
|
|
|
1071
1073
|
return false;
|
|
1072
1074
|
}
|
|
1073
1075
|
|
|
1074
|
-
/**
|
|
1075
|
-
* A teammate deleted a message: the visitor sees "This message was deleted"
|
|
1076
|
-
* in its place (they can't delete anything themselves), and the list is
|
|
1077
|
-
* refreshed so its preview stops showing the text. The id stays in
|
|
1078
|
-
* `seenMessageIds`, so a replayed `message.new` can't restore the content.
|
|
1079
|
-
*/
|
|
1080
1076
|
/**
|
|
1081
1077
|
* A teammate opened the conversation: the visitor's messages are now Seen.
|
|
1082
1078
|
* The ids are also remembered, because the event can arrive before the send
|
|
@@ -1086,9 +1082,15 @@ export class WidgetStore {
|
|
|
1086
1082
|
const ids: string[] = Array.isArray(data?.messageIds) ? data.messageIds : [];
|
|
1087
1083
|
const status: string | undefined = typeof data?.status === 'string' ? data.status : undefined;
|
|
1088
1084
|
if (!status || ids.length === 0) return;
|
|
1089
|
-
for (const id of ids)
|
|
1090
|
-
|
|
1091
|
-
this.earlyStatuses.delete(
|
|
1085
|
+
for (const id of ids) {
|
|
1086
|
+
// Re-inserted, so a repeated id counts as newest when trimming.
|
|
1087
|
+
this.earlyStatuses.delete(id);
|
|
1088
|
+
this.earlyStatuses.set(id, status);
|
|
1089
|
+
}
|
|
1090
|
+
// One event can carry many ids: trim back to the cap, oldest first.
|
|
1091
|
+
for (const id of this.earlyStatuses.keys()) {
|
|
1092
|
+
if (this.earlyStatuses.size <= SEEN_MESSAGE_LIMIT) break;
|
|
1093
|
+
this.earlyStatuses.delete(id);
|
|
1092
1094
|
}
|
|
1093
1095
|
if (!this.isThreadOnScreen(data?.conversationId)) return;
|
|
1094
1096
|
this.setState((current) => ({
|
|
@@ -1098,6 +1100,12 @@ export class WidgetStore {
|
|
|
1098
1100
|
}));
|
|
1099
1101
|
}
|
|
1100
1102
|
|
|
1103
|
+
/**
|
|
1104
|
+
* A teammate deleted a message: the visitor sees "This message was deleted"
|
|
1105
|
+
* in its place (they can't delete anything themselves), and the list is
|
|
1106
|
+
* refreshed so its preview stops showing the text. The id stays in
|
|
1107
|
+
* `seenMessageIds`, so a replayed `message.new` can't restore the content.
|
|
1108
|
+
*/
|
|
1101
1109
|
private handleDeletedMessage(data: any) {
|
|
1102
1110
|
const messageId: string | undefined = data?.messageId;
|
|
1103
1111
|
if (!messageId) return;
|
|
@@ -1263,7 +1271,7 @@ export class WidgetStore {
|
|
|
1263
1271
|
body: null,
|
|
1264
1272
|
bodyPlain: data.bodyPlain || 'Conversation updated',
|
|
1265
1273
|
attachments: null,
|
|
1266
|
-
metadata: data.metadata || null,
|
|
1274
|
+
metadata: this.hydrateMetadata(data.metadata || null),
|
|
1267
1275
|
status: 'delivered',
|
|
1268
1276
|
createdAt: data.createdAt || new Date().toISOString(),
|
|
1269
1277
|
};
|
|
@@ -1667,10 +1675,35 @@ export class WidgetStore {
|
|
|
1667
1675
|
.reverse();
|
|
1668
1676
|
}
|
|
1669
1677
|
|
|
1670
|
-
/**
|
|
1678
|
+
/**
|
|
1679
|
+
* Media URLs inside metadata (the "joined" line's avatar, product photos in
|
|
1680
|
+
* agent blocks) can be server-relative paths, and are remote content: like
|
|
1681
|
+
* every other media URL they resolve against `baseUrl`, and anything that
|
|
1682
|
+
* isn't http(s) is dropped before it reaches an `<Image>`.
|
|
1683
|
+
*/
|
|
1671
1684
|
private hydrateMetadata(metadata: Message['metadata']): Message['metadata'] {
|
|
1672
|
-
if (!metadata
|
|
1673
|
-
|
|
1685
|
+
if (!metadata) return null;
|
|
1686
|
+
let next = metadata;
|
|
1687
|
+
if (typeof metadata.agentAvatar === 'string') {
|
|
1688
|
+
next = { ...next, agentAvatar: this.client.resolveUrl(metadata.agentAvatar) };
|
|
1689
|
+
}
|
|
1690
|
+
if (Array.isArray(metadata.agentUiBlocks)) {
|
|
1691
|
+
next = {
|
|
1692
|
+
...next,
|
|
1693
|
+
agentUiBlocks: metadata.agentUiBlocks.map((block: unknown) => this.hydrateBlockMedia(block)),
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
return next;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
private hydrateBlockMedia(block: unknown): unknown {
|
|
1700
|
+
const candidate = block as { type?: unknown; product?: { imageUrl?: unknown } } | null;
|
|
1701
|
+
const imageUrl = candidate?.product?.imageUrl;
|
|
1702
|
+
if (candidate?.type !== 'product_service_card' || typeof imageUrl !== 'string') return block;
|
|
1703
|
+
return {
|
|
1704
|
+
...candidate,
|
|
1705
|
+
product: { ...candidate.product, imageUrl: this.client.resolveUrl(imageUrl) ?? undefined },
|
|
1706
|
+
};
|
|
1674
1707
|
}
|
|
1675
1708
|
|
|
1676
1709
|
/**
|
|
@@ -1967,11 +2000,6 @@ function isLocalMessage(message: Message): boolean {
|
|
|
1967
2000
|
return message.id.startsWith('temp-');
|
|
1968
2001
|
}
|
|
1969
2002
|
|
|
1970
|
-
/**
|
|
1971
|
-
* A sent visitor message keeps its local previews (a `file://` URI, or no URL
|
|
1972
|
-
* at all) until the server copy shows up — the stream echo or a history page.
|
|
1973
|
-
* Its attachments, the only ones that can be opened, then replace them.
|
|
1974
|
-
*/
|
|
1975
2003
|
/** A "Seen" missed while the stream was down shows up on the next re-read. */
|
|
1976
2004
|
function adoptServerStatuses(messages: Message[], server: Message[]): Message[] {
|
|
1977
2005
|
const seen = new Set(server.filter((message) => message.status === 'read').map((message) => message.id));
|
|
@@ -2006,6 +2034,11 @@ function adoptServerDeletions(messages: Message[], server: Message[]): Message[]
|
|
|
2006
2034
|
return changed ? next : messages;
|
|
2007
2035
|
}
|
|
2008
2036
|
|
|
2037
|
+
/**
|
|
2038
|
+
* A sent visitor message keeps its local previews (a `file://` URI, or no URL
|
|
2039
|
+
* at all) until the server copy shows up — the stream echo or a history page.
|
|
2040
|
+
* Its attachments, the only ones that can be opened, then replace them.
|
|
2041
|
+
*/
|
|
2009
2042
|
function adoptServerAttachments(messages: Message[], server: Message[]): Message[] {
|
|
2010
2043
|
const serverCopies = new Map(
|
|
2011
2044
|
server.filter((message) => message.attachments?.length).map((message) => [message.id, message])
|
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,10 +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;
|
|
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
|
+
*/
|
|
175
|
+
signedUpAt?: Date | number | string;
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
/** The user's account (company) in your app, sent as `WidgetUser.account`. @since 1.0.8 */
|
|
171
179
|
export interface WidgetUserAccount {
|
|
172
180
|
/** Your app's stable account id — required. */
|
|
173
181
|
id: string;
|
|
@@ -488,13 +496,6 @@ export interface RepzoWidgetStorageAdapter {
|
|
|
488
496
|
removeItem(key: string): Promise<void> | void;
|
|
489
497
|
}
|
|
490
498
|
|
|
491
|
-
/**
|
|
492
|
-
* `RepzoWidgetError.code`. The last two come from the server's 403 body and
|
|
493
|
-
* are terminal — the SDK stops realtime and does not retry on its own:
|
|
494
|
-
* - `messenger_api_disabled` — the channel's Messenger API is switched off.
|
|
495
|
-
* - `identity_verification_failed` — `userHash` missing/wrong while the
|
|
496
|
-
* channel enforces identity verification.
|
|
497
|
-
*/
|
|
498
499
|
/** A bad-rating reason on the rating card, labelled per language. */
|
|
499
500
|
export interface RatingReason {
|
|
500
501
|
id: string;
|
|
@@ -514,6 +515,13 @@ export interface ConversationRatingResult {
|
|
|
514
515
|
locked: boolean;
|
|
515
516
|
}
|
|
516
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
|
+
*/
|
|
517
525
|
export type RepzoWidgetErrorCode =
|
|
518
526
|
| 'network'
|
|
519
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
|
/>
|