@kavchat/react-native 0.0.2 → 0.0.3
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/README.md +38 -0
- package/lib/controller.d.ts +101 -2
- package/lib/controller.js +333 -4
- package/lib/index.d.ts +4 -1
- package/lib/live/api.d.ts +93 -0
- package/lib/live/api.js +41 -0
- package/lib/live/session.d.ts +183 -0
- package/lib/live/session.js +702 -0
- package/lib/live/sse.d.ts +56 -0
- package/lib/live/sse.js +173 -0
- package/lib/live/texts.d.ts +8 -0
- package/lib/live/texts.js +25 -0
- package/lib/ui/KavchatChat.js +20 -8
- package/lib/ui/KavchatProvider.d.ts +11 -0
- package/lib/ui/KavchatProvider.js +16 -2
- package/lib/ui/LiveChatView.d.ts +8 -0
- package/lib/ui/LiveChatView.js +146 -0
- package/package.json +2 -2
- package/src/controller.ts +371 -6
- package/src/index.ts +4 -1
- package/src/live/api.ts +109 -0
- package/src/live/session.ts +784 -0
- package/src/live/sse.ts +196 -0
- package/src/live/texts.ts +25 -0
- package/src/ui/KavchatChat.tsx +37 -8
- package/src/ui/KavchatProvider.tsx +21 -2
- package/src/ui/LiveChatView.tsx +308 -0
package/src/live/sse.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* לקוח SSE קטן, בלי EventSource (ב-React Native אין כזה) ובלי חיבור מחדש אוטומטי:
|
|
3
|
+
* הכרטיס חד-פעמי, ולכן ה-session מבקש כרטיס חדש ומתחבר מחדש בעצמו (docs/live-chat-protocol.md 3.4).
|
|
4
|
+
* התעבורה: XMLHttpRequest עם קבלה הדרגתית (נתמך ב-React Native וב-Hermes), או fetch עם body.getReader
|
|
5
|
+
* כשאין XMLHttpRequest. אם אין אף אחד, ה-session עובר ל-long-poll
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SseMessage {
|
|
9
|
+
event: string;
|
|
10
|
+
data: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** מפרק את הזרם לפי המפרט: שורות field: value, הודעה מסתיימת בשורה ריקה, שורה שמתחילה ב-: היא הערה */
|
|
15
|
+
export class SseParser {
|
|
16
|
+
private buffer = '';
|
|
17
|
+
private event = '';
|
|
18
|
+
private data: string[] = [];
|
|
19
|
+
private id: string | undefined;
|
|
20
|
+
|
|
21
|
+
constructor(private onMessage: (m: SseMessage) => void, private onComment?: () => void) {}
|
|
22
|
+
|
|
23
|
+
feed(chunk: string) {
|
|
24
|
+
this.buffer += chunk;
|
|
25
|
+
for (;;) {
|
|
26
|
+
const m = /\r\n|\r|\n/.exec(this.buffer);
|
|
27
|
+
if (!m) break;
|
|
28
|
+
// \r בסוף החתיכה עשוי להיות חצי של \r\n: מחכים לחתיכה הבאה
|
|
29
|
+
if (m[0] === '\r' && m.index === this.buffer.length - 1) break;
|
|
30
|
+
const line = this.buffer.slice(0, m.index);
|
|
31
|
+
this.buffer = this.buffer.slice(m.index + m[0].length);
|
|
32
|
+
this.line(line);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private line(line: string) {
|
|
37
|
+
if (line === '') {
|
|
38
|
+
if (this.data.length) this.onMessage({ event: this.event || 'message', data: this.data.join('\n'), ...(this.id !== undefined ? { id: this.id } : {}) });
|
|
39
|
+
this.event = '';
|
|
40
|
+
this.data = [];
|
|
41
|
+
this.id = undefined;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (line.startsWith(':')) {
|
|
45
|
+
this.onComment?.();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const i = line.indexOf(':');
|
|
49
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
50
|
+
let value = i < 0 ? '' : line.slice(i + 1);
|
|
51
|
+
if (value.startsWith(' ')) value = value.slice(1);
|
|
52
|
+
if (field === 'event') this.event = value;
|
|
53
|
+
else if (field === 'data') this.data.push(value);
|
|
54
|
+
else if (field === 'id') this.id = value;
|
|
55
|
+
// retry ושדות אחרים: מתעלמים. החיבור מחדש בשליטה שלנו
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface StreamHandlers {
|
|
60
|
+
/** כל חתיכת טקסט שהגיעה, כולל הערות keep-alive */
|
|
61
|
+
onChunk(text: string): void;
|
|
62
|
+
/** הזרם נכשל לפני שנפתח (status != 200), או נקטע. status 0 = רשת */
|
|
63
|
+
onError(status: number, body: string): void;
|
|
64
|
+
/** השרת סגר את הזרם בצורה מסודרת */
|
|
65
|
+
onEnd(): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface StreamHandle {
|
|
69
|
+
close(): void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** פותח GET ארוך ומעביר טקסט כפי שהוא מגיע. null כשאין תעבורה מתאימה, ואז עוברים ל-long-poll */
|
|
73
|
+
export type StreamOpener = (url: string, headers: Record<string, string>, handlers: StreamHandlers) => StreamHandle | null;
|
|
74
|
+
|
|
75
|
+
type XhrLike = {
|
|
76
|
+
readyState: number;
|
|
77
|
+
status: number;
|
|
78
|
+
responseText: string;
|
|
79
|
+
open(method: string, url: string, async: boolean): void;
|
|
80
|
+
setRequestHeader(name: string, value: string): void;
|
|
81
|
+
send(body?: unknown): void;
|
|
82
|
+
abort(): void;
|
|
83
|
+
onreadystatechange: (() => void) | null;
|
|
84
|
+
onprogress: (() => void) | null;
|
|
85
|
+
onerror: (() => void) | null;
|
|
86
|
+
ontimeout?: (() => void) | null;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** XMLHttpRequest עם responseText שגדל (readyState 3). מספיק להגדיר onprogress כדי ש-React Native ישלח עדכונים הדרגתיים */
|
|
90
|
+
export const xhrStream: StreamOpener = (url, headers, h) => {
|
|
91
|
+
const Xhr = (globalThis as { XMLHttpRequest?: new () => XhrLike }).XMLHttpRequest;
|
|
92
|
+
if (!Xhr) return null;
|
|
93
|
+
const xhr = new Xhr();
|
|
94
|
+
let seen = 0;
|
|
95
|
+
let done = false;
|
|
96
|
+
const pump = () => {
|
|
97
|
+
if (done || xhr.status !== 200) return;
|
|
98
|
+
const text = xhr.responseText || '';
|
|
99
|
+
if (text.length > seen) {
|
|
100
|
+
const chunk = text.slice(seen);
|
|
101
|
+
seen = text.length;
|
|
102
|
+
h.onChunk(chunk);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
xhr.onprogress = pump;
|
|
106
|
+
xhr.onreadystatechange = () => {
|
|
107
|
+
if (done) return;
|
|
108
|
+
if (xhr.readyState >= 3) pump();
|
|
109
|
+
if (xhr.readyState === 4) {
|
|
110
|
+
done = true;
|
|
111
|
+
if (xhr.status === 200) h.onEnd();
|
|
112
|
+
else h.onError(xhr.status, xhr.responseText || '');
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
xhr.onerror = () => {
|
|
116
|
+
if (done) return;
|
|
117
|
+
done = true;
|
|
118
|
+
h.onError(0, '');
|
|
119
|
+
};
|
|
120
|
+
xhr.ontimeout = xhr.onerror;
|
|
121
|
+
try {
|
|
122
|
+
xhr.open('GET', url, true);
|
|
123
|
+
for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
|
|
124
|
+
xhr.setRequestHeader('Accept', 'text/event-stream');
|
|
125
|
+
xhr.send();
|
|
126
|
+
} catch {
|
|
127
|
+
done = true;
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
close() {
|
|
132
|
+
if (done) return;
|
|
133
|
+
done = true;
|
|
134
|
+
try {
|
|
135
|
+
xhr.abort();
|
|
136
|
+
} catch {
|
|
137
|
+
// כבר נסגר
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type StreamingResponse = {
|
|
144
|
+
status: number;
|
|
145
|
+
text(): Promise<string>;
|
|
146
|
+
body?: { getReader?: () => { read(): Promise<{ done: boolean; value?: Uint8Array }>; cancel(): Promise<void> } } | null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** fetch עם ReadableStream (Expo web, סביבות חדשות). בלי getReader אין זרימה, ומחזירים שגיאה כדי לעבור ל-long-poll */
|
|
150
|
+
export function fetchStream(fetchImpl?: (url: string, init: Record<string, unknown>) => Promise<StreamingResponse>): StreamOpener {
|
|
151
|
+
return (url, headers, h) => {
|
|
152
|
+
const f = fetchImpl ?? (globalThis.fetch as unknown as ((url: string, init: Record<string, unknown>) => Promise<StreamingResponse>) | undefined);
|
|
153
|
+
const Ctrl = (globalThis as { AbortController?: new () => { abort(): void; signal: unknown } }).AbortController;
|
|
154
|
+
const Decoder = (globalThis as { TextDecoder?: new () => { decode(v?: Uint8Array, o?: { stream: boolean }): string } }).TextDecoder;
|
|
155
|
+
if (!f || !Decoder) return null;
|
|
156
|
+
const ctrl = Ctrl ? new Ctrl() : null;
|
|
157
|
+
let done = false;
|
|
158
|
+
const finish = (fn: () => void) => {
|
|
159
|
+
if (done) return;
|
|
160
|
+
done = true;
|
|
161
|
+
fn();
|
|
162
|
+
};
|
|
163
|
+
void (async () => {
|
|
164
|
+
try {
|
|
165
|
+
const res = await f(url, { method: 'GET', headers: { ...headers, Accept: 'text/event-stream' }, cache: 'no-store', credentials: 'omit', ...(ctrl ? { signal: ctrl.signal } : {}) });
|
|
166
|
+
if (res.status !== 200) {
|
|
167
|
+
const body = await res.text().catch(() => '');
|
|
168
|
+
return finish(() => h.onError(res.status, body));
|
|
169
|
+
}
|
|
170
|
+
const reader = res.body?.getReader?.();
|
|
171
|
+
if (!reader) return finish(() => h.onError(-1, ''));
|
|
172
|
+
const decoder = new Decoder();
|
|
173
|
+
for (;;) {
|
|
174
|
+
const { done: end, value } = await reader.read();
|
|
175
|
+
if (done) {
|
|
176
|
+
void reader.cancel().catch(() => undefined);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (end) return finish(() => h.onEnd());
|
|
180
|
+
if (value) h.onChunk(decoder.decode(value, { stream: true }));
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
finish(() => h.onError(0, ''));
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
return {
|
|
187
|
+
close() {
|
|
188
|
+
done = true;
|
|
189
|
+
ctrl?.abort();
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** ברירת המחדל: XMLHttpRequest אם קיים, אחרת fetch עם זרימה */
|
|
196
|
+
export const defaultStreamOpener: StreamOpener = (url, headers, h) => xhrStream(url, headers, h) ?? fetchStream()(url, headers, h);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* הטקסטים עצמם ב-@kavchat/core (i18n, המפתחות live.*), כמו בווידג'ט לאתר. כאן רק עיצוב של זמן
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* מתי הנציגים חוזרים, בשעון המכשיר ובשפת הממשק: רק שעה אם זה היום, אחרת יום ושעה (כמו בווידג'ט).
|
|
9
|
+
* בלי Intl (Hermes ישן): HH:MM או DD/MM HH:MM
|
|
10
|
+
*/
|
|
11
|
+
export function formatOpenAt(iso: string, now: number, locale?: string): string {
|
|
12
|
+
const at = new Date(iso);
|
|
13
|
+
if (Number.isNaN(at.getTime())) return '';
|
|
14
|
+
const today = new Date(now);
|
|
15
|
+
const sameDay = at.getFullYear() === today.getFullYear() && at.getMonth() === today.getMonth() && at.getDate() === today.getDate();
|
|
16
|
+
if (locale) {
|
|
17
|
+
try {
|
|
18
|
+
return new Intl.DateTimeFormat(locale, { ...(sameDay ? {} : { weekday: 'short' }), hour: '2-digit', minute: '2-digit' }).format(at);
|
|
19
|
+
} catch {
|
|
20
|
+
// ממשיכים לפורמט הקבוע
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const time = `${pad(at.getHours())}:${pad(at.getMinutes())}`;
|
|
24
|
+
return sameDay ? time : `${pad(at.getDate())}/${pad(at.getMonth() + 1)} ${time}`;
|
|
25
|
+
}
|
package/src/ui/KavchatChat.tsx
CHANGED
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
View,
|
|
15
15
|
} from 'react-native';
|
|
16
16
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
17
|
-
import { format } from '@kavchat/core';
|
|
17
|
+
import { format, liveUnavailableText } from '@kavchat/core';
|
|
18
18
|
import type { BotChip, BotMessage } from '@kavchat/core';
|
|
19
19
|
import { BRANDING_URL } from '../controller';
|
|
20
20
|
import { safeUrl, splitLinks } from '../text';
|
|
21
21
|
import type { KavchatTheme } from '../theme';
|
|
22
22
|
import { useKavchatContext } from './KavchatProvider';
|
|
23
|
+
import { LiveChatView, LiveRequestForm } from './LiveChatView';
|
|
24
|
+
import { formatOpenAt } from '../live/texts';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* מסך הצ'אט. כיוון הפריסה נקבע לפי I18nManager של האפליקציה המארחת, ולא כופים RTL.
|
|
@@ -86,7 +88,17 @@ export function KavchatChat({ onClose, hideHeader, style }: KavchatChatProps) {
|
|
|
86
88
|
<Text style={styles.bannerText}>{t['widget.offline']}</Text>
|
|
87
89
|
</View>
|
|
88
90
|
) : null}
|
|
89
|
-
{snapshot.view === 'chat' ?
|
|
91
|
+
{snapshot.view === 'chat' ? (
|
|
92
|
+
<ChatView />
|
|
93
|
+
) : snapshot.view === 'form' ? (
|
|
94
|
+
<ContactForm />
|
|
95
|
+
) : snapshot.view === 'live' ? (
|
|
96
|
+
<LiveChatView />
|
|
97
|
+
) : snapshot.view === 'liveForm' ? (
|
|
98
|
+
<LiveRequestForm />
|
|
99
|
+
) : (
|
|
100
|
+
<SentView />
|
|
101
|
+
)}
|
|
90
102
|
{s.removeBranding ? null : (
|
|
91
103
|
// בתוכנית החינמית הסמל חובה
|
|
92
104
|
<Pressable onPress={() => Linking.openURL(BRANDING_URL).catch(() => undefined)} accessibilityRole="link" style={styles.badge}>
|
|
@@ -103,6 +115,8 @@ function ChatView() {
|
|
|
103
115
|
const [text, setText] = useState('');
|
|
104
116
|
const scroll = useRef<ComponentRef<typeof ScrollView>>(null);
|
|
105
117
|
const conv = snapshot.conversation;
|
|
118
|
+
// כולל "שיחה עם נציג" לפני "פנייה לצוות" כשיש נציג זמין
|
|
119
|
+
const chips = conv ? controller.chips() : [];
|
|
106
120
|
|
|
107
121
|
const typing = snapshot.typing;
|
|
108
122
|
const send = () => {
|
|
@@ -129,9 +143,9 @@ function ChatView() {
|
|
|
129
143
|
<ActivityIndicator size="small" color={theme.accent} />
|
|
130
144
|
</View>
|
|
131
145
|
) : null}
|
|
132
|
-
{
|
|
146
|
+
{chips.length > 0 ? (
|
|
133
147
|
<View style={styles.chips} accessibilityRole="menu">
|
|
134
|
-
{
|
|
148
|
+
{chips.map((chip) => (
|
|
135
149
|
<ChipButton key={chip.id} label={chip.label} theme={theme} fixed={isFixed(chip)} onPress={() => controller.pressChip(chip)} />
|
|
136
150
|
))}
|
|
137
151
|
</View>
|
|
@@ -216,11 +230,13 @@ function ContactForm() {
|
|
|
216
230
|
const effect = snapshot.contact;
|
|
217
231
|
const topics = snapshot.release?.settings.contactTopics ?? [];
|
|
218
232
|
const initialTopic = effect?.contactTopicId && topics.some((x) => x.id === effect.contactTopicId) ? effect.contactTopicId : '';
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
const [
|
|
233
|
+
// אחרי בקשה לנציג שלא נענתה: מה שכבר נכתב שם
|
|
234
|
+
const pre = snapshot.contactPrefill;
|
|
235
|
+
const [name, setName] = useState(pre?.name ?? snapshot.userName ?? '');
|
|
236
|
+
const [email, setEmail] = useState(pre?.email ?? '');
|
|
237
|
+
const [phone, setPhone] = useState(pre?.phone ?? '');
|
|
222
238
|
const [topicId, setTopicId] = useState(initialTopic);
|
|
223
|
-
const [message, setMessage] = useState(effect?.prefill ?? '');
|
|
239
|
+
const [message, setMessage] = useState(pre?.message ?? effect?.prefill ?? '');
|
|
224
240
|
const [error, setError] = useState('');
|
|
225
241
|
const [sending, setSending] = useState(false);
|
|
226
242
|
|
|
@@ -233,12 +249,24 @@ function ContactForm() {
|
|
|
233
249
|
if (problem) setError(problem);
|
|
234
250
|
};
|
|
235
251
|
|
|
252
|
+
// כשהצ'אט עם נציג דלוק ואין מי שיענה: שורת הסבר מעל הטופס (closed, no_agents). quota ו-disabled בלי הסבר
|
|
253
|
+
const offer = snapshot.liveEnabled ? snapshot.liveOffer : null;
|
|
254
|
+
const note =
|
|
255
|
+
offer && !offer.open && !offer.checking
|
|
256
|
+
? liveUnavailableText(offer.reason, t, offer.reason === 'closed' && offer.nextOpenAt ? formatOpenAt(offer.nextOpenAt, Date.now(), snapshot.locale) || null : null)
|
|
257
|
+
: '';
|
|
258
|
+
|
|
236
259
|
const inputStyle = [styles.field, { color: theme.text }];
|
|
237
260
|
return (
|
|
238
261
|
<ScrollView style={styles.flex} contentContainerStyle={styles.form} keyboardShouldPersistTaps="handled">
|
|
239
262
|
<Text style={[styles.formTitle, { color: theme.text }]} accessibilityRole="header">
|
|
240
263
|
{t['form.title']}
|
|
241
264
|
</Text>
|
|
265
|
+
{note ? (
|
|
266
|
+
<Text style={[styles.reason, { color: theme.muted }]} accessibilityLiveRegion="polite">
|
|
267
|
+
{note}
|
|
268
|
+
</Text>
|
|
269
|
+
) : null}
|
|
242
270
|
<Label text={t['form.name']} theme={theme} />
|
|
243
271
|
<TextInput value={name} onChangeText={setName} maxLength={100} autoComplete="name" textContentType="name" style={inputStyle} accessibilityLabel={t['form.name']} />
|
|
244
272
|
<Label text={t['form.email']} theme={theme} />
|
|
@@ -373,6 +401,7 @@ const styles = StyleSheet.create({
|
|
|
373
401
|
field: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, paddingHorizontal: 10, paddingVertical: Platform.OS === 'ios' ? 10 : 6, fontSize: 15, backgroundColor: '#ffffff' },
|
|
374
402
|
message: { minHeight: 110, textAlignVertical: 'top' },
|
|
375
403
|
error: { fontSize: 14, marginTop: 6 },
|
|
404
|
+
reason: { fontSize: 14, marginTop: 8 },
|
|
376
405
|
actions: { flexDirection: 'row', gap: 10, marginTop: 12 },
|
|
377
406
|
btn: { borderRadius: 8, paddingHorizontal: 18, paddingVertical: 10, alignItems: 'center' },
|
|
378
407
|
btnGhost: { borderWidth: 1, backgroundColor: 'transparent' },
|
|
@@ -28,6 +28,13 @@ export interface KavchatProviderProps {
|
|
|
28
28
|
onEvent?: (event: KavchatEvent) => void;
|
|
29
29
|
/** ברירת מחדל: AsyncStorage אם מותקן, אחרת זיכרון בלבד */
|
|
30
30
|
storage?: StorageAdapter;
|
|
31
|
+
/**
|
|
32
|
+
* אחסון לקרדנשל של הצ'אט עם נציג. מומלץ אחסון מאובטח (expo-secure-store, react-native-keychain).
|
|
33
|
+
* ברירת מחדל: storage
|
|
34
|
+
*/
|
|
35
|
+
secureStorage?: StorageAdapter;
|
|
36
|
+
/** שם המסך שנשלח עם הבקשה לנציג, למשל 'checkout' */
|
|
37
|
+
livePage?: string;
|
|
31
38
|
/** עקיפה של הצבעים שמגיעים מההגדרות בלוח הניהול */
|
|
32
39
|
theme?: Partial<KavchatTheme>;
|
|
33
40
|
/** להחלפה בבדיקות */
|
|
@@ -48,7 +55,7 @@ export interface KavchatContextValue {
|
|
|
48
55
|
const KavchatContext = createContext<KavchatContextValue | null>(null);
|
|
49
56
|
|
|
50
57
|
export function KavchatProvider(props: KavchatProviderProps) {
|
|
51
|
-
const { publicKey, apiBase, locale, userToken, userName, onScreen, screens, onEvent, storage, theme, fetch, children } = props;
|
|
58
|
+
const { publicKey, apiBase, locale, userToken, userName, onScreen, screens, onEvent, storage, secureStorage, livePage, theme, fetch, children } = props;
|
|
52
59
|
const controllerRef = useRef<KavchatController | null>(null);
|
|
53
60
|
const keyRef = useRef('');
|
|
54
61
|
const key = `${publicKey}|${apiBase ?? ''}`;
|
|
@@ -62,6 +69,8 @@ export function KavchatProvider(props: KavchatProviderProps) {
|
|
|
62
69
|
userToken: userToken || undefined,
|
|
63
70
|
userName: userName || undefined,
|
|
64
71
|
storage: storage ?? defaultStorage(),
|
|
72
|
+
secureStorage,
|
|
73
|
+
livePage,
|
|
65
74
|
deviceLocale: deviceLocale(),
|
|
66
75
|
screens,
|
|
67
76
|
fetch,
|
|
@@ -107,10 +116,13 @@ export function KavchatProvider(props: KavchatProviderProps) {
|
|
|
107
116
|
controller.setLocale(locale);
|
|
108
117
|
}, [controller, locale]);
|
|
109
118
|
|
|
110
|
-
// דיווח המסלול כשהאפליקציה עוברת לרקע, כי ייתכן שלא
|
|
119
|
+
// דיווח המסלול כשהאפליקציה עוברת לרקע, כי ייתכן שלא תחזור.
|
|
120
|
+
// הצ'אט עם הנציג: הזרם נסגר ברקע, ובחזרה מושלם לפי seq (אין התראות push בגרסה הזו)
|
|
111
121
|
useEffect(() => {
|
|
112
122
|
const sub = AppState.addEventListener('change', (s) => {
|
|
113
123
|
if (s === 'background' || s === 'inactive') void controller.flush();
|
|
124
|
+
if (s === 'background') controller.setForeground(false);
|
|
125
|
+
if (s === 'active') controller.setForeground(true);
|
|
114
126
|
});
|
|
115
127
|
return () => sub.remove();
|
|
116
128
|
}, [controller]);
|
|
@@ -161,6 +173,13 @@ export function useKavchat() {
|
|
|
161
173
|
open();
|
|
162
174
|
controller.openContactForm(prefill);
|
|
163
175
|
},
|
|
176
|
+
/** "שיחה עם נציג" מכפתור באפליקציה: טופס הבקשה אם יש נציג זמין, אחרת טופס הפנייה עם הסבר */
|
|
177
|
+
openLive: () => {
|
|
178
|
+
open();
|
|
179
|
+
void controller.openLive();
|
|
180
|
+
},
|
|
181
|
+
/** שלב הצ'אט עם הנציג (queued, active...), או null */
|
|
182
|
+
livePhase: snapshot.live?.phase ?? null,
|
|
164
183
|
controller,
|
|
165
184
|
};
|
|
166
185
|
}
|