@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.
@@ -0,0 +1,56 @@
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
+ export interface SseMessage {
8
+ event: string;
9
+ data: string;
10
+ id?: string;
11
+ }
12
+ /** מפרק את הזרם לפי המפרט: שורות field: value, הודעה מסתיימת בשורה ריקה, שורה שמתחילה ב-: היא הערה */
13
+ export declare class SseParser {
14
+ private onMessage;
15
+ private onComment?;
16
+ private buffer;
17
+ private event;
18
+ private data;
19
+ private id;
20
+ constructor(onMessage: (m: SseMessage) => void, onComment?: (() => void) | undefined);
21
+ feed(chunk: string): void;
22
+ private line;
23
+ }
24
+ export interface StreamHandlers {
25
+ /** כל חתיכת טקסט שהגיעה, כולל הערות keep-alive */
26
+ onChunk(text: string): void;
27
+ /** הזרם נכשל לפני שנפתח (status != 200), או נקטע. status 0 = רשת */
28
+ onError(status: number, body: string): void;
29
+ /** השרת סגר את הזרם בצורה מסודרת */
30
+ onEnd(): void;
31
+ }
32
+ export interface StreamHandle {
33
+ close(): void;
34
+ }
35
+ /** פותח GET ארוך ומעביר טקסט כפי שהוא מגיע. null כשאין תעבורה מתאימה, ואז עוברים ל-long-poll */
36
+ export type StreamOpener = (url: string, headers: Record<string, string>, handlers: StreamHandlers) => StreamHandle | null;
37
+ /** XMLHttpRequest עם responseText שגדל (readyState 3). מספיק להגדיר onprogress כדי ש-React Native ישלח עדכונים הדרגתיים */
38
+ export declare const xhrStream: StreamOpener;
39
+ type StreamingResponse = {
40
+ status: number;
41
+ text(): Promise<string>;
42
+ body?: {
43
+ getReader?: () => {
44
+ read(): Promise<{
45
+ done: boolean;
46
+ value?: Uint8Array;
47
+ }>;
48
+ cancel(): Promise<void>;
49
+ };
50
+ } | null;
51
+ };
52
+ /** fetch עם ReadableStream (Expo web, סביבות חדשות). בלי getReader אין זרימה, ומחזירים שגיאה כדי לעבור ל-long-poll */
53
+ export declare function fetchStream(fetchImpl?: (url: string, init: Record<string, unknown>) => Promise<StreamingResponse>): StreamOpener;
54
+ /** ברירת המחדל: XMLHttpRequest אם קיים, אחרת fetch עם זרימה */
55
+ export declare const defaultStreamOpener: StreamOpener;
56
+ export {};
@@ -0,0 +1,173 @@
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
+ /** מפרק את הזרם לפי המפרט: שורות field: value, הודעה מסתיימת בשורה ריקה, שורה שמתחילה ב-: היא הערה */
8
+ export class SseParser {
9
+ constructor(onMessage, onComment) {
10
+ this.onMessage = onMessage;
11
+ this.onComment = onComment;
12
+ this.buffer = '';
13
+ this.event = '';
14
+ this.data = [];
15
+ }
16
+ feed(chunk) {
17
+ this.buffer += chunk;
18
+ for (;;) {
19
+ const m = /\r\n|\r|\n/.exec(this.buffer);
20
+ if (!m)
21
+ break;
22
+ // \r בסוף החתיכה עשוי להיות חצי של \r\n: מחכים לחתיכה הבאה
23
+ if (m[0] === '\r' && m.index === this.buffer.length - 1)
24
+ break;
25
+ const line = this.buffer.slice(0, m.index);
26
+ this.buffer = this.buffer.slice(m.index + m[0].length);
27
+ this.line(line);
28
+ }
29
+ }
30
+ line(line) {
31
+ if (line === '') {
32
+ if (this.data.length)
33
+ this.onMessage({ event: this.event || 'message', data: this.data.join('\n'), ...(this.id !== undefined ? { id: this.id } : {}) });
34
+ this.event = '';
35
+ this.data = [];
36
+ this.id = undefined;
37
+ return;
38
+ }
39
+ if (line.startsWith(':')) {
40
+ this.onComment?.();
41
+ return;
42
+ }
43
+ const i = line.indexOf(':');
44
+ const field = i < 0 ? line : line.slice(0, i);
45
+ let value = i < 0 ? '' : line.slice(i + 1);
46
+ if (value.startsWith(' '))
47
+ value = value.slice(1);
48
+ if (field === 'event')
49
+ this.event = value;
50
+ else if (field === 'data')
51
+ this.data.push(value);
52
+ else if (field === 'id')
53
+ this.id = value;
54
+ // retry ושדות אחרים: מתעלמים. החיבור מחדש בשליטה שלנו
55
+ }
56
+ }
57
+ /** XMLHttpRequest עם responseText שגדל (readyState 3). מספיק להגדיר onprogress כדי ש-React Native ישלח עדכונים הדרגתיים */
58
+ export const xhrStream = (url, headers, h) => {
59
+ const Xhr = globalThis.XMLHttpRequest;
60
+ if (!Xhr)
61
+ return null;
62
+ const xhr = new Xhr();
63
+ let seen = 0;
64
+ let done = false;
65
+ const pump = () => {
66
+ if (done || xhr.status !== 200)
67
+ return;
68
+ const text = xhr.responseText || '';
69
+ if (text.length > seen) {
70
+ const chunk = text.slice(seen);
71
+ seen = text.length;
72
+ h.onChunk(chunk);
73
+ }
74
+ };
75
+ xhr.onprogress = pump;
76
+ xhr.onreadystatechange = () => {
77
+ if (done)
78
+ return;
79
+ if (xhr.readyState >= 3)
80
+ pump();
81
+ if (xhr.readyState === 4) {
82
+ done = true;
83
+ if (xhr.status === 200)
84
+ h.onEnd();
85
+ else
86
+ h.onError(xhr.status, xhr.responseText || '');
87
+ }
88
+ };
89
+ xhr.onerror = () => {
90
+ if (done)
91
+ return;
92
+ done = true;
93
+ h.onError(0, '');
94
+ };
95
+ xhr.ontimeout = xhr.onerror;
96
+ try {
97
+ xhr.open('GET', url, true);
98
+ for (const [k, v] of Object.entries(headers))
99
+ xhr.setRequestHeader(k, v);
100
+ xhr.setRequestHeader('Accept', 'text/event-stream');
101
+ xhr.send();
102
+ }
103
+ catch {
104
+ done = true;
105
+ return null;
106
+ }
107
+ return {
108
+ close() {
109
+ if (done)
110
+ return;
111
+ done = true;
112
+ try {
113
+ xhr.abort();
114
+ }
115
+ catch {
116
+ // כבר נסגר
117
+ }
118
+ },
119
+ };
120
+ };
121
+ /** fetch עם ReadableStream (Expo web, סביבות חדשות). בלי getReader אין זרימה, ומחזירים שגיאה כדי לעבור ל-long-poll */
122
+ export function fetchStream(fetchImpl) {
123
+ return (url, headers, h) => {
124
+ const f = fetchImpl ?? globalThis.fetch;
125
+ const Ctrl = globalThis.AbortController;
126
+ const Decoder = globalThis.TextDecoder;
127
+ if (!f || !Decoder)
128
+ return null;
129
+ const ctrl = Ctrl ? new Ctrl() : null;
130
+ let done = false;
131
+ const finish = (fn) => {
132
+ if (done)
133
+ return;
134
+ done = true;
135
+ fn();
136
+ };
137
+ void (async () => {
138
+ try {
139
+ const res = await f(url, { method: 'GET', headers: { ...headers, Accept: 'text/event-stream' }, cache: 'no-store', credentials: 'omit', ...(ctrl ? { signal: ctrl.signal } : {}) });
140
+ if (res.status !== 200) {
141
+ const body = await res.text().catch(() => '');
142
+ return finish(() => h.onError(res.status, body));
143
+ }
144
+ const reader = res.body?.getReader?.();
145
+ if (!reader)
146
+ return finish(() => h.onError(-1, ''));
147
+ const decoder = new Decoder();
148
+ for (;;) {
149
+ const { done: end, value } = await reader.read();
150
+ if (done) {
151
+ void reader.cancel().catch(() => undefined);
152
+ return;
153
+ }
154
+ if (end)
155
+ return finish(() => h.onEnd());
156
+ if (value)
157
+ h.onChunk(decoder.decode(value, { stream: true }));
158
+ }
159
+ }
160
+ catch {
161
+ finish(() => h.onError(0, ''));
162
+ }
163
+ })();
164
+ return {
165
+ close() {
166
+ done = true;
167
+ ctrl?.abort();
168
+ },
169
+ };
170
+ };
171
+ }
172
+ /** ברירת המחדל: XMLHttpRequest אם קיים, אחרת fetch עם זרימה */
173
+ export const defaultStreamOpener = (url, headers, h) => xhrStream(url, headers, h) ?? fetchStream()(url, headers, h);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * הטקסטים עצמם ב-@kavchat/core (i18n, המפתחות live.*), כמו בווידג'ט לאתר. כאן רק עיצוב של זמן
3
+ */
4
+ /**
5
+ * מתי הנציגים חוזרים, בשעון המכשיר ובשפת הממשק: רק שעה אם זה היום, אחרת יום ושעה (כמו בווידג'ט).
6
+ * בלי Intl (Hermes ישן): HH:MM או DD/MM HH:MM
7
+ */
8
+ export declare function formatOpenAt(iso: string, now: number, locale?: string): string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * הטקסטים עצמם ב-@kavchat/core (i18n, המפתחות live.*), כמו בווידג'ט לאתר. כאן רק עיצוב של זמן
3
+ */
4
+ const pad = (n) => String(n).padStart(2, '0');
5
+ /**
6
+ * מתי הנציגים חוזרים, בשעון המכשיר ובשפת הממשק: רק שעה אם זה היום, אחרת יום ושעה (כמו בווידג'ט).
7
+ * בלי Intl (Hermes ישן): HH:MM או DD/MM HH:MM
8
+ */
9
+ export function formatOpenAt(iso, now, locale) {
10
+ const at = new Date(iso);
11
+ if (Number.isNaN(at.getTime()))
12
+ return '';
13
+ const today = new Date(now);
14
+ const sameDay = at.getFullYear() === today.getFullYear() && at.getMonth() === today.getMonth() && at.getDate() === today.getDate();
15
+ if (locale) {
16
+ try {
17
+ return new Intl.DateTimeFormat(locale, { ...(sameDay ? {} : { weekday: 'short' }), hour: '2-digit', minute: '2-digit' }).format(at);
18
+ }
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
+ }
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { ActivityIndicator, Image, KeyboardAvoidingView, Linking, Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View, } from 'react-native';
4
- import { format } from '@kavchat/core';
4
+ import { format, liveUnavailableText } from '@kavchat/core';
5
5
  import { BRANDING_URL } from '../controller';
6
6
  import { safeUrl, splitLinks } from '../text';
7
7
  import { useKavchatContext } from './KavchatProvider';
8
+ import { LiveChatView, LiveRequestForm } from './LiveChatView';
9
+ import { formatOpenAt } from '../live/texts';
8
10
  export function KavchatChat({ onClose, hideHeader, style }) {
9
11
  const { controller, snapshot, theme } = useKavchatContext();
10
12
  const t = snapshot.texts;
@@ -18,7 +20,7 @@ export function KavchatChat({ onClose, hideHeader, style }) {
18
20
  return (_jsxs(View, { style: [styles.center, { backgroundColor: theme.background }, style], children: [_jsx(Text, { style: [styles.body, { color: theme.text }], children: t['widget.unavailable'] }), onClose ? _jsx(ChipButton, { label: t['widget.close'], theme: theme, onPress: onClose }) : null] }));
19
21
  }
20
22
  const s = snapshot.release.settings;
21
- return (_jsxs(KeyboardAvoidingView, { style: [styles.flex, { backgroundColor: theme.background }, style], behavior: Platform.OS === 'ios' ? 'padding' : undefined, children: [hideHeader ? null : (_jsxs(View, { style: [styles.header, { backgroundColor: theme.primary }], children: [_jsx(Avatar, { text: s.avatarText || s.botName || '', imageUrl: s.avatarImageUrl, theme: theme }), _jsxs(View, { style: styles.titles, children: [_jsx(Text, { style: [styles.title, { color: theme.onPrimary }], numberOfLines: 1, accessibilityRole: "header", children: s.botName }), s.subtitle ? (_jsx(Text, { style: [styles.subtitle, { color: theme.onPrimary }], numberOfLines: 1, children: s.subtitle })) : null] }), onClose ? (_jsx(Pressable, { onPress: onClose, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: t['widget.close'], style: styles.close, children: _jsx(Text, { style: [styles.closeText, { color: theme.onPrimary }], children: '×' }) })) : null] })), snapshot.offline ? (_jsx(View, { style: styles.banner, accessibilityRole: "alert", children: _jsx(Text, { style: styles.bannerText, children: t['widget.offline'] }) })) : null, snapshot.view === 'chat' ? _jsx(ChatView, {}) : snapshot.view === 'form' ? _jsx(ContactForm, {}) : _jsx(SentView, {}), s.removeBranding ? null : (
23
+ return (_jsxs(KeyboardAvoidingView, { style: [styles.flex, { backgroundColor: theme.background }, style], behavior: Platform.OS === 'ios' ? 'padding' : undefined, children: [hideHeader ? null : (_jsxs(View, { style: [styles.header, { backgroundColor: theme.primary }], children: [_jsx(Avatar, { text: s.avatarText || s.botName || '', imageUrl: s.avatarImageUrl, theme: theme }), _jsxs(View, { style: styles.titles, children: [_jsx(Text, { style: [styles.title, { color: theme.onPrimary }], numberOfLines: 1, accessibilityRole: "header", children: s.botName }), s.subtitle ? (_jsx(Text, { style: [styles.subtitle, { color: theme.onPrimary }], numberOfLines: 1, children: s.subtitle })) : null] }), onClose ? (_jsx(Pressable, { onPress: onClose, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: t['widget.close'], style: styles.close, children: _jsx(Text, { style: [styles.closeText, { color: theme.onPrimary }], children: '×' }) })) : null] })), snapshot.offline ? (_jsx(View, { style: styles.banner, accessibilityRole: "alert", children: _jsx(Text, { style: styles.bannerText, children: t['widget.offline'] }) })) : null, snapshot.view === 'chat' ? (_jsx(ChatView, {})) : snapshot.view === 'form' ? (_jsx(ContactForm, {})) : snapshot.view === 'live' ? (_jsx(LiveChatView, {})) : snapshot.view === 'liveForm' ? (_jsx(LiveRequestForm, {})) : (_jsx(SentView, {})), s.removeBranding ? null : (
22
24
  // בתוכנית החינמית הסמל חובה
23
25
  _jsx(Pressable, { onPress: () => Linking.openURL(BRANDING_URL).catch(() => undefined), accessibilityRole: "link", style: styles.badge, children: _jsx(Text, { style: [styles.badgeText, { color: theme.muted }], children: t['widget.poweredBy'] }) }))] }));
24
26
  }
@@ -28,6 +30,8 @@ function ChatView() {
28
30
  const [text, setText] = useState('');
29
31
  const scroll = useRef(null);
30
32
  const conv = snapshot.conversation;
33
+ // כולל "שיחה עם נציג" לפני "פנייה לצוות" כשיש נציג זמין
34
+ const chips = conv ? controller.chips() : [];
31
35
  const typing = snapshot.typing;
32
36
  const send = () => {
33
37
  if (typing)
@@ -36,7 +40,7 @@ function ChatView() {
36
40
  setText('');
37
41
  void controller.sendText(value);
38
42
  };
39
- return (_jsxs(View, { style: styles.flex, children: [_jsxs(ScrollView, { ref: scroll, style: styles.flex, contentContainerStyle: styles.list, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [(conv?.messages ?? []).map((m) => (_jsx(Bubble, { message: m, theme: theme, aiLabel: t['chat.aiLabel'] }, m.id))), typing ? (_jsx(View, { style: [styles.bubble, styles.botBubble, { backgroundColor: theme.botBubble }], accessibilityLabel: t['widget.loading'], accessibilityLiveRegion: "polite", children: _jsx(ActivityIndicator, { size: "small", color: theme.accent }) })) : null, conv && conv.chips.length > 0 ? (_jsx(View, { style: styles.chips, accessibilityRole: "menu", children: conv.chips.map((chip) => (_jsx(ChipButton, { label: chip.label, theme: theme, fixed: isFixed(chip), onPress: () => controller.pressChip(chip) }, chip.id))) })) : null] }), _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { value: text, onChangeText: setText, onSubmitEditing: send, placeholder: t['widget.placeholder'], accessibilityLabel: t['widget.placeholder'], placeholderTextColor: theme.muted, maxLength: 500, returnKeyType: "send", blurOnSubmit: false, style: [styles.input, { color: theme.text }] }), _jsx(Pressable, { onPress: send, disabled: !text.trim() || typing, accessibilityRole: "button", accessibilityLabel: t['widget.send'], style: [styles.send, { backgroundColor: theme.primary }, (!text.trim() || typing) && styles.disabled], children: _jsx(SendIcon, { color: theme.onPrimary }) })] })] }));
43
+ return (_jsxs(View, { style: styles.flex, children: [_jsxs(ScrollView, { ref: scroll, style: styles.flex, contentContainerStyle: styles.list, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [(conv?.messages ?? []).map((m) => (_jsx(Bubble, { message: m, theme: theme, aiLabel: t['chat.aiLabel'] }, m.id))), typing ? (_jsx(View, { style: [styles.bubble, styles.botBubble, { backgroundColor: theme.botBubble }], accessibilityLabel: t['widget.loading'], accessibilityLiveRegion: "polite", children: _jsx(ActivityIndicator, { size: "small", color: theme.accent }) })) : null, chips.length > 0 ? (_jsx(View, { style: styles.chips, accessibilityRole: "menu", children: chips.map((chip) => (_jsx(ChipButton, { label: chip.label, theme: theme, fixed: isFixed(chip), onPress: () => controller.pressChip(chip) }, chip.id))) })) : null] }), _jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { value: text, onChangeText: setText, onSubmitEditing: send, placeholder: t['widget.placeholder'], accessibilityLabel: t['widget.placeholder'], placeholderTextColor: theme.muted, maxLength: 500, returnKeyType: "send", blurOnSubmit: false, style: [styles.input, { color: theme.text }] }), _jsx(Pressable, { onPress: send, disabled: !text.trim() || typing, accessibilityRole: "button", accessibilityLabel: t['widget.send'], style: [styles.send, { backgroundColor: theme.primary }, (!text.trim() || typing) && styles.disabled], children: _jsx(SendIcon, { color: theme.onPrimary }) })] })] }));
40
44
  }
41
45
  const isFixed = (chip) => chip.kind === 'back' || chip.kind === 'home';
42
46
  function Bubble({ message, theme, aiLabel }) {
@@ -62,11 +66,13 @@ function ContactForm() {
62
66
  const effect = snapshot.contact;
63
67
  const topics = snapshot.release?.settings.contactTopics ?? [];
64
68
  const initialTopic = effect?.contactTopicId && topics.some((x) => x.id === effect.contactTopicId) ? effect.contactTopicId : '';
65
- const [name, setName] = useState(snapshot.userName ?? '');
66
- const [email, setEmail] = useState('');
67
- const [phone, setPhone] = useState('');
69
+ // אחרי בקשה לנציג שלא נענתה: מה שכבר נכתב שם
70
+ const pre = snapshot.contactPrefill;
71
+ const [name, setName] = useState(pre?.name ?? snapshot.userName ?? '');
72
+ const [email, setEmail] = useState(pre?.email ?? '');
73
+ const [phone, setPhone] = useState(pre?.phone ?? '');
68
74
  const [topicId, setTopicId] = useState(initialTopic);
69
- const [message, setMessage] = useState(effect?.prefill ?? '');
75
+ const [message, setMessage] = useState(pre?.message ?? effect?.prefill ?? '');
70
76
  const [error, setError] = useState('');
71
77
  const [sending, setSending] = useState(false);
72
78
  const submit = async () => {
@@ -79,8 +85,13 @@ function ContactForm() {
79
85
  if (problem)
80
86
  setError(problem);
81
87
  };
88
+ // כשהצ'אט עם נציג דלוק ואין מי שיענה: שורת הסבר מעל הטופס (closed, no_agents). quota ו-disabled בלי הסבר
89
+ const offer = snapshot.liveEnabled ? snapshot.liveOffer : null;
90
+ const note = offer && !offer.open && !offer.checking
91
+ ? liveUnavailableText(offer.reason, t, offer.reason === 'closed' && offer.nextOpenAt ? formatOpenAt(offer.nextOpenAt, Date.now(), snapshot.locale) || null : null)
92
+ : '';
82
93
  const inputStyle = [styles.field, { color: theme.text }];
83
- return (_jsxs(ScrollView, { style: styles.flex, contentContainerStyle: styles.form, keyboardShouldPersistTaps: "handled", children: [_jsx(Text, { style: [styles.formTitle, { color: theme.text }], accessibilityRole: "header", children: t['form.title'] }), _jsx(Label, { text: t['form.name'], theme: theme }), _jsx(TextInput, { value: name, onChangeText: setName, maxLength: 100, autoComplete: "name", textContentType: "name", style: inputStyle, accessibilityLabel: t['form.name'] }), _jsx(Label, { text: t['form.email'], theme: theme }), _jsx(TextInput, { value: email, onChangeText: setEmail, keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress", style: inputStyle, accessibilityLabel: t['form.email'] }), _jsx(Label, { text: t['form.phone'], theme: theme }), _jsx(TextInput, { value: phone, onChangeText: setPhone, keyboardType: "phone-pad", autoComplete: "tel", textContentType: "telephoneNumber", style: inputStyle, accessibilityLabel: t['form.phone'] }), topics.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Label, { text: t['form.topic'], theme: theme }), _jsx(View, { style: styles.chips, accessibilityRole: "radiogroup", children: topics.map((tp) => {
94
+ return (_jsxs(ScrollView, { style: styles.flex, contentContainerStyle: styles.form, keyboardShouldPersistTaps: "handled", children: [_jsx(Text, { style: [styles.formTitle, { color: theme.text }], accessibilityRole: "header", children: t['form.title'] }), note ? (_jsx(Text, { style: [styles.reason, { color: theme.muted }], accessibilityLiveRegion: "polite", children: note })) : null, _jsx(Label, { text: t['form.name'], theme: theme }), _jsx(TextInput, { value: name, onChangeText: setName, maxLength: 100, autoComplete: "name", textContentType: "name", style: inputStyle, accessibilityLabel: t['form.name'] }), _jsx(Label, { text: t['form.email'], theme: theme }), _jsx(TextInput, { value: email, onChangeText: setEmail, keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress", style: inputStyle, accessibilityLabel: t['form.email'] }), _jsx(Label, { text: t['form.phone'], theme: theme }), _jsx(TextInput, { value: phone, onChangeText: setPhone, keyboardType: "phone-pad", autoComplete: "tel", textContentType: "telephoneNumber", style: inputStyle, accessibilityLabel: t['form.phone'] }), topics.length > 0 ? (_jsxs(_Fragment, { children: [_jsx(Label, { text: t['form.topic'], theme: theme }), _jsx(View, { style: styles.chips, accessibilityRole: "radiogroup", children: topics.map((tp) => {
84
95
  const on = tp.id === topicId;
85
96
  return (_jsx(Pressable, { onPress: () => setTopicId(on ? '' : tp.id), accessibilityRole: "radio", accessibilityState: { checked: on }, style: [styles.chip, { borderColor: theme.accent }, on && { backgroundColor: theme.primary, borderColor: theme.primary }], children: _jsx(Text, { style: [styles.chipText, { color: on ? theme.onPrimary : theme.accent }], children: tp.name }) }, tp.id));
86
97
  }) })] })) : null, _jsx(Label, { text: t['form.message'], theme: theme }), _jsx(TextInput, { value: message, onChangeText: setMessage, multiline: true, maxLength: 2000, style: [inputStyle, styles.message], accessibilityLabel: t['form.message'] }), error ? (_jsx(Text, { style: [styles.error, { color: theme.error }], accessibilityRole: "alert", accessibilityLiveRegion: "polite", children: error })) : null, _jsxs(View, { style: styles.actions, children: [_jsx(Pressable, { onPress: submit, disabled: sending, accessibilityRole: "button", accessibilityState: { disabled: sending, busy: sending }, style: [styles.btn, { backgroundColor: theme.primary }, sending && styles.disabled], children: _jsx(Text, { style: [styles.btnText, { color: theme.onPrimary }], children: sending ? t['form.sending'] : t['form.submit'] }) }), _jsx(Pressable, { onPress: () => controller.backToChat(), accessibilityRole: "button", style: [styles.btn, styles.btnGhost, { borderColor: theme.accent }], children: _jsx(Text, { style: [styles.btnText, { color: theme.accent }], children: t['form.cancel'] }) })] })] }));
@@ -140,6 +151,7 @@ const styles = StyleSheet.create({
140
151
  field: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, paddingHorizontal: 10, paddingVertical: Platform.OS === 'ios' ? 10 : 6, fontSize: 15, backgroundColor: '#ffffff' },
141
152
  message: { minHeight: 110, textAlignVertical: 'top' },
142
153
  error: { fontSize: 14, marginTop: 6 },
154
+ reason: { fontSize: 14, marginTop: 8 },
143
155
  actions: { flexDirection: 'row', gap: 10, marginTop: 12 },
144
156
  btn: { borderRadius: 8, paddingHorizontal: 18, paddingVertical: 10, alignItems: 'center' },
145
157
  btnGhost: { borderWidth: 1, backgroundColor: 'transparent' },
@@ -24,6 +24,13 @@ export interface KavchatProviderProps {
24
24
  onEvent?: (event: KavchatEvent) => void;
25
25
  /** ברירת מחדל: AsyncStorage אם מותקן, אחרת זיכרון בלבד */
26
26
  storage?: StorageAdapter;
27
+ /**
28
+ * אחסון לקרדנשל של הצ'אט עם נציג. מומלץ אחסון מאובטח (expo-secure-store, react-native-keychain).
29
+ * ברירת מחדל: storage
30
+ */
31
+ secureStorage?: StorageAdapter;
32
+ /** שם המסך שנשלח עם הבקשה לנציג, למשל 'checkout' */
33
+ livePage?: string;
27
34
  /** עקיפה של הצבעים שמגיעים מההגדרות בלוח הניהול */
28
35
  theme?: Partial<KavchatTheme>;
29
36
  /** להחלפה בבדיקות */
@@ -58,5 +65,9 @@ export declare function useKavchat(): {
58
65
  } | null) => Promise<void>;
59
66
  startNewConversation: () => void;
60
67
  openContactForm: (prefill?: string) => void;
68
+ /** "שיחה עם נציג" מכפתור באפליקציה: טופס הבקשה אם יש נציג זמין, אחרת טופס הפנייה עם הסבר */
69
+ openLive: () => void;
70
+ /** שלב הצ'אט עם הנציג (queued, active...), או null */
71
+ livePhase: import("..").LivePhase | null;
61
72
  controller: KavchatController;
62
73
  };
@@ -7,7 +7,7 @@ import { deviceLocale } from '../text';
7
7
  import { themeFor } from '../theme';
8
8
  const KavchatContext = createContext(null);
9
9
  export function KavchatProvider(props) {
10
- const { publicKey, apiBase, locale, userToken, userName, onScreen, screens, onEvent, storage, theme, fetch, children } = props;
10
+ const { publicKey, apiBase, locale, userToken, userName, onScreen, screens, onEvent, storage, secureStorage, livePage, theme, fetch, children } = props;
11
11
  const controllerRef = useRef(null);
12
12
  const keyRef = useRef('');
13
13
  const key = `${publicKey}|${apiBase ?? ''}`;
@@ -21,6 +21,8 @@ export function KavchatProvider(props) {
21
21
  userToken: userToken || undefined,
22
22
  userName: userName || undefined,
23
23
  storage: storage ?? defaultStorage(),
24
+ secureStorage,
25
+ livePage,
24
26
  deviceLocale: deviceLocale(),
25
27
  screens,
26
28
  fetch,
@@ -60,11 +62,16 @@ export function KavchatProvider(props) {
60
62
  useEffect(() => {
61
63
  controller.setLocale(locale);
62
64
  }, [controller, locale]);
63
- // דיווח המסלול כשהאפליקציה עוברת לרקע, כי ייתכן שלא תחזור
65
+ // דיווח המסלול כשהאפליקציה עוברת לרקע, כי ייתכן שלא תחזור.
66
+ // הצ'אט עם הנציג: הזרם נסגר ברקע, ובחזרה מושלם לפי seq (אין התראות push בגרסה הזו)
64
67
  useEffect(() => {
65
68
  const sub = AppState.addEventListener('change', (s) => {
66
69
  if (s === 'background' || s === 'inactive')
67
70
  void controller.flush();
71
+ if (s === 'background')
72
+ controller.setForeground(false);
73
+ if (s === 'active')
74
+ controller.setForeground(true);
68
75
  });
69
76
  return () => sub.remove();
70
77
  }, [controller]);
@@ -108,6 +115,13 @@ export function useKavchat() {
108
115
  open();
109
116
  controller.openContactForm(prefill);
110
117
  },
118
+ /** "שיחה עם נציג" מכפתור באפליקציה: טופס הבקשה אם יש נציג זמין, אחרת טופס הפנייה עם הסבר */
119
+ openLive: () => {
120
+ open();
121
+ void controller.openLive();
122
+ },
123
+ /** שלב הצ'אט עם הנציג (queued, active...), או null */
124
+ livePhase: snapshot.live?.phase ?? null,
111
125
  controller,
112
126
  };
113
127
  }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * הצ'אט עם הנציג: שורת מצב (המתנה עם ספירה לאחור, שם הנציג, או המצב הסופי), הודעות, הקלדה,
3
+ * "להשאיר הודעה במקום", סיום, וחזרה לבוט. הטקסטים והשורות מ-@kavchat/core, כמו בווידג'ט לאתר.
4
+ * טקסט פשוט בלבד: הודעות הנציג לא הופכות לקישורים
5
+ */
6
+ export declare function LiveChatView(): import("react").JSX.Element | null;
7
+ /** טופס הבקשה לנציג: אורח משאיר שם ומייל או טלפון, בחירת מחלקה אם יש יותר מאחת, והודעה ראשונה (לא חובה) */
8
+ export declare function LiveRequestForm(): import("react").JSX.Element;
@@ -0,0 +1,146 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Platform, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
4
+ import { LIVE_MESSAGE_MAX, liveLines, liveStatus } from '@kavchat/core';
5
+ import { useKavchatContext } from './KavchatProvider';
6
+ /**
7
+ * הצ'אט עם הנציג: שורת מצב (המתנה עם ספירה לאחור, שם הנציג, או המצב הסופי), הודעות, הקלדה,
8
+ * "להשאיר הודעה במקום", סיום, וחזרה לבוט. הטקסטים והשורות מ-@kavchat/core, כמו בווידג'ט לאתר.
9
+ * טקסט פשוט בלבד: הודעות הנציג לא הופכות לקישורים
10
+ */
11
+ export function LiveChatView() {
12
+ const { controller, snapshot, theme } = useKavchatContext();
13
+ const live = snapshot.live;
14
+ const t = snapshot.texts;
15
+ const [text, setText] = useState('');
16
+ const [error, setError] = useState('');
17
+ const [busy, setBusy] = useState(false);
18
+ const [now, setNow] = useState(Date.now());
19
+ const scroll = useRef(null);
20
+ const queued = live?.phase === 'queued';
21
+ // הספירה לאחור מתעדכנת כל שנייה, רק בזמן ההמתנה
22
+ useEffect(() => {
23
+ if (!queued)
24
+ return;
25
+ const id = setInterval(() => setNow(Date.now()), 1000);
26
+ return () => clearInterval(id);
27
+ }, [queued]);
28
+ if (!live)
29
+ return null;
30
+ const open = live.phase === 'queued' || live.phase === 'active';
31
+ const send = () => {
32
+ const problem = controller.liveSend(text);
33
+ if (problem) {
34
+ // הטקסט נשאר בשדה
35
+ setError(problem);
36
+ return;
37
+ }
38
+ setError('');
39
+ setText('');
40
+ };
41
+ const run = async (fn) => {
42
+ if (busy)
43
+ return;
44
+ setBusy(true);
45
+ try {
46
+ await fn();
47
+ }
48
+ finally {
49
+ setBusy(false);
50
+ }
51
+ };
52
+ return (_jsxs(View, { style: styles.flex, children: [_jsxs(View, { style: styles.status, children: [_jsx(Text, { style: [styles.statusText, { color: theme.text }], accessibilityRole: "header", accessibilityLiveRegion: "polite", children: statusText(live, t, now) }), open ? (_jsx(Pressable, { onPress: () => run(() => controller.liveEnd()), disabled: busy, accessibilityRole: "button", hitSlop: 8, style: [styles.small, { borderColor: theme.accent }], children: _jsx(Text, { style: [styles.smallText, { color: theme.accent }], children: t['live.end'] }) })) : null] }), live.connection === 'reconnecting' && open ? (_jsx(View, { style: styles.banner, accessibilityRole: "alert", children: _jsx(Text, { style: styles.bannerText, children: t['live.reconnecting'] }) })) : null, _jsxs(ScrollView, { ref: scroll, style: styles.flex, contentContainerStyle: styles.list, onContentSizeChange: () => scroll.current?.scrollToEnd({ animated: true }), keyboardShouldPersistTaps: "handled", children: [liveLines(live.chat, t).map((line) => (_jsx(Line, { line: line, theme: theme, t: t, onRetry: line.clientId ? () => controller.liveRetry(line.clientId) : undefined }, line.key))), live.agentTyping !== null && live.phase === 'active' ? (_jsx(Text, { style: [styles.typing, { color: theme.muted }], accessibilityLiveRegion: "polite", children: t['live.typing'] })) : null, queued ? (_jsx(Pressable, { onPress: () => run(() => controller.liveLeaveMessage()), disabled: busy, accessibilityRole: "button", style: [styles.btn, styles.btnGhost, styles.leave, { borderColor: theme.accent }, busy && styles.disabled], children: _jsx(Text, { style: [styles.btnText, { color: theme.accent }], children: t['live.leave'] }) })) : null, !open ? (_jsx(View, { style: styles.final, children: _jsx(Pressable, { onPress: () => controller.closeLive(), accessibilityRole: "button", style: [styles.btn, { backgroundColor: theme.primary }], children: _jsx(Text, { style: [styles.btnText, { color: theme.onPrimary }], children: t['live.back'] }) }) })) : null] }), error ? (_jsx(Text, { style: [styles.error, { color: theme.error }], accessibilityRole: "alert", children: error })) : null, open ? (_jsxs(View, { style: styles.composer, children: [_jsx(TextInput, { value: text, onChangeText: (v) => {
53
+ setText(v);
54
+ controller.liveTyping(v);
55
+ }, onSubmitEditing: send, placeholder: t['live.placeholder'], accessibilityLabel: t['live.placeholder'], placeholderTextColor: theme.muted, maxLength: LIVE_MESSAGE_MAX, returnKeyType: "send", blurOnSubmit: false, style: [styles.input, { color: theme.text }] }), _jsx(Pressable, { onPress: send, disabled: !text.trim(), accessibilityRole: "button", accessibilityLabel: t['widget.send'], style: [styles.send, { backgroundColor: theme.primary }, !text.trim() && styles.disabled], children: _jsx(View, { style: [styles.sendIcon, { borderStartColor: theme.onPrimary }] }) })] })) : null] }));
56
+ }
57
+ /** שורת המצב של המכונה המשותפת, ושני מצבים שלא באים מהשרת: הוחלף במכשיר אחר, או פג */
58
+ function statusText(live, t, now) {
59
+ if (live.phase === 'replaced')
60
+ return t['live.replaced'];
61
+ if (live.phase === 'expired')
62
+ return t['live.ended'];
63
+ return liveStatus(live.conversation, t, now);
64
+ }
65
+ function Line({ line, theme, t, onRetry }) {
66
+ if (line.kind === 'system')
67
+ return _jsx(Text, { style: [styles.system, { color: theme.muted }], children: line.text });
68
+ const mine = line.kind === 'visitor';
69
+ const failed = line.status === 'failed';
70
+ const bubble = (_jsxs(_Fragment, { children: [!mine && line.name ? _jsx(Text, { style: [styles.author, { color: theme.muted }], children: line.name }) : null, _jsx(Text, { style: [styles.body, { color: theme.text }], selectable: !line.status, children: line.text }), failed ? _jsx(Text, { style: [styles.meta, { color: theme.error }], children: t['live.failed'] }) : null] }));
71
+ const style = [styles.bubble, mine ? [styles.mine, { backgroundColor: theme.userBubble }] : [styles.theirs, { backgroundColor: theme.botBubble }], line.status === 'sending' && styles.sending];
72
+ if (failed && onRetry) {
73
+ return (_jsx(Pressable, { onPress: onRetry, accessibilityRole: "button", accessibilityHint: t['live.failed'], style: style, children: bubble }));
74
+ }
75
+ return _jsx(View, { style: style, children: bubble });
76
+ }
77
+ /** טופס הבקשה לנציג: אורח משאיר שם ומייל או טלפון, בחירת מחלקה אם יש יותר מאחת, והודעה ראשונה (לא חובה) */
78
+ export function LiveRequestForm() {
79
+ const { controller, snapshot, theme } = useKavchatContext();
80
+ const t = snapshot.texts;
81
+ const guest = !snapshot.identified;
82
+ const departments = snapshot.liveOffer?.departments ?? [];
83
+ const [name, setName] = useState(snapshot.userName ?? '');
84
+ const [email, setEmail] = useState('');
85
+ const [phone, setPhone] = useState('');
86
+ const [message, setMessage] = useState(snapshot.contact?.prefill ?? '');
87
+ const [departmentId, setDepartmentId] = useState(departments[0]?.id ?? '');
88
+ const [error, setError] = useState('');
89
+ const [sending, setSending] = useState(false);
90
+ const submit = async () => {
91
+ if (sending)
92
+ return;
93
+ setSending(true);
94
+ setError('');
95
+ const problem = await controller.requestLive({ name, email, phone, message, departmentId: departmentId || undefined });
96
+ setSending(false);
97
+ if (problem)
98
+ setError(problem);
99
+ };
100
+ const field = [styles.field, { color: theme.text }];
101
+ return (_jsxs(ScrollView, { style: styles.flex, contentContainerStyle: styles.form, keyboardShouldPersistTaps: "handled", children: [_jsx(Text, { style: [styles.formTitle, { color: theme.text }], accessibilityRole: "header", children: t['live.talk'] }), guest ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [styles.note, { color: theme.muted }], children: t['live.intro'] }), _jsx(Text, { style: [styles.label, { color: theme.muted }], children: t['form.name'] }), _jsx(TextInput, { value: name, onChangeText: setName, maxLength: 100, autoComplete: "name", textContentType: "name", style: field, accessibilityLabel: t['form.name'] }), _jsx(Text, { style: [styles.label, { color: theme.muted }], children: t['form.email'] }), _jsx(TextInput, { value: email, onChangeText: setEmail, keyboardType: "email-address", autoCapitalize: "none", autoComplete: "email", textContentType: "emailAddress", style: field, accessibilityLabel: t['form.email'] }), _jsx(Text, { style: [styles.label, { color: theme.muted }], children: t['form.phone'] }), _jsx(TextInput, { value: phone, onChangeText: setPhone, keyboardType: "phone-pad", autoComplete: "tel", textContentType: "telephoneNumber", style: field, accessibilityLabel: t['form.phone'] })] })) : null, departments.length > 1 ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: [styles.label, { color: theme.muted }], children: t['live.department'] }), _jsx(View, { style: styles.chips, accessibilityRole: "radiogroup", children: departments.map((d) => {
102
+ const on = d.id === departmentId;
103
+ return (_jsx(Pressable, { onPress: () => setDepartmentId(d.id), accessibilityRole: "radio", accessibilityState: { checked: on }, style: [styles.chip, { borderColor: theme.accent }, on && { backgroundColor: theme.primary, borderColor: theme.primary }], children: _jsx(Text, { style: [styles.chipText, { color: on ? theme.onPrimary : theme.accent }], children: d.name }) }, d.id));
104
+ }) })] })) : null, _jsx(Text, { style: [styles.label, { color: theme.muted }], children: t['live.message'] }), _jsx(TextInput, { value: message, onChangeText: setMessage, multiline: true, maxLength: LIVE_MESSAGE_MAX, style: [field, styles.message], accessibilityLabel: t['live.message'] }), error ? (_jsx(Text, { style: [styles.error, { color: theme.error }], accessibilityRole: "alert", accessibilityLiveRegion: "polite", children: error })) : null, _jsxs(View, { style: styles.actions, children: [_jsx(Pressable, { onPress: submit, disabled: sending, accessibilityRole: "button", accessibilityState: { disabled: sending, busy: sending }, style: [styles.btn, { backgroundColor: theme.primary }, sending && styles.disabled], children: _jsx(Text, { style: [styles.btnText, { color: theme.onPrimary }], children: sending ? t['live.connecting'] : t['live.start'] }) }), _jsx(Pressable, { onPress: () => controller.backToChat(), accessibilityRole: "button", style: [styles.btn, styles.btnGhost, { borderColor: theme.accent }], children: _jsx(Text, { style: [styles.btnText, { color: theme.accent }], children: t['form.cancel'] }) })] })] }));
105
+ }
106
+ const styles = StyleSheet.create({
107
+ flex: { flex: 1 },
108
+ status: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#e5e7eb', backgroundColor: '#ffffff' },
109
+ statusText: { flex: 1, fontSize: 14, fontWeight: '600' },
110
+ small: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 10, paddingVertical: 4 },
111
+ smallText: { fontSize: 13, fontWeight: '600' },
112
+ banner: { backgroundColor: '#fef3c7', paddingHorizontal: 12, paddingVertical: 6 },
113
+ bannerText: { color: '#92400e', fontSize: 13 },
114
+ list: { padding: 12, gap: 6 },
115
+ bubble: { maxWidth: '85%', borderRadius: 14, paddingHorizontal: 12, paddingVertical: 8 },
116
+ // המבקר בצד הסוף והנציג בצד ההתחלה, כמו בצ'אט עם הבוט. flex-start מתהפך לבד ב-RTL
117
+ theirs: { alignSelf: 'flex-start', borderWidth: StyleSheet.hairlineWidth, borderColor: '#e5e7eb' },
118
+ mine: { alignSelf: 'flex-end' },
119
+ sending: { opacity: 0.6 },
120
+ author: { fontSize: 12, fontWeight: '600', marginBottom: 2 },
121
+ body: { fontSize: 15, lineHeight: 21 },
122
+ meta: { fontSize: 12, marginTop: 2 },
123
+ system: { alignSelf: 'center', fontSize: 13, textAlign: 'center', paddingVertical: 4 },
124
+ typing: { alignSelf: 'flex-start', fontSize: 13, fontStyle: 'italic', paddingVertical: 2 },
125
+ leave: { alignSelf: 'center', marginTop: 8 },
126
+ final: { alignItems: 'center', paddingVertical: 12 },
127
+ error: { fontSize: 14, paddingHorizontal: 12, paddingVertical: 4 },
128
+ composer: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 8, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: '#e5e7eb', backgroundColor: '#ffffff' },
129
+ input: { flex: 1, borderRadius: 20, backgroundColor: '#f3f4f6', paddingHorizontal: 14, paddingVertical: Platform.OS === 'ios' ? 10 : 6, fontSize: 15 },
130
+ send: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
131
+ sendIcon: { width: 0, height: 0, borderTopWidth: 8, borderBottomWidth: 8, borderStartWidth: 14, borderTopColor: 'transparent', borderBottomColor: 'transparent', marginStart: 4 },
132
+ disabled: { opacity: 0.5 },
133
+ btn: { borderRadius: 8, paddingHorizontal: 18, paddingVertical: 10, alignItems: 'center' },
134
+ btnGhost: { borderWidth: 1, backgroundColor: 'transparent' },
135
+ btnText: { fontSize: 15, fontWeight: '600' },
136
+ form: { padding: 16, gap: 6 },
137
+ formTitle: { fontSize: 18, fontWeight: '600', marginBottom: 6 },
138
+ note: { fontSize: 14, marginBottom: 4 },
139
+ label: { fontSize: 13, marginTop: 6 },
140
+ field: { borderWidth: 1, borderColor: '#d1d5db', borderRadius: 8, paddingHorizontal: 10, paddingVertical: Platform.OS === 'ios' ? 10 : 6, fontSize: 15, backgroundColor: '#ffffff' },
141
+ message: { minHeight: 90, textAlignVertical: 'top' },
142
+ chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 6 },
143
+ chip: { borderWidth: 1.5, borderRadius: 999, paddingHorizontal: 14, paddingVertical: 7, backgroundColor: '#ffffff' },
144
+ chipText: { fontSize: 14, fontWeight: '600' },
145
+ actions: { flexDirection: 'row', gap: 10, marginTop: 12 },
146
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kavchat/react-native",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Kavchat for React Native: the scripted support chat as a provider, a chat screen and a floating launcher.",
5
5
  "license": "MIT",
6
6
  "main": "lib/index.js",
@@ -19,7 +19,7 @@
19
19
  "typecheck": "tsc --noEmit"
20
20
  },
21
21
  "dependencies": {
22
- "@kavchat/core": "0.0.2"
22
+ "@kavchat/core": "0.0.3"
23
23
  },
24
24
  "peerDependencies": {
25
25
  "@react-native-async-storage/async-storage": ">=1.17.0",