@kavchat/react-native 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +180 -2
- package/lib/api.d.ts +87 -0
- package/lib/api.js +112 -0
- package/lib/controller.d.ts +147 -0
- package/lib/controller.js +419 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.js +6 -0
- package/lib/screens.d.ts +30 -0
- package/lib/screens.js +120 -0
- package/lib/storage.d.ts +35 -0
- package/lib/storage.js +70 -0
- package/lib/text.d.ts +21 -0
- package/lib/text.js +86 -0
- package/lib/theme.d.ts +21 -0
- package/lib/theme.js +53 -0
- package/lib/ui/KavchatChat.d.ts +13 -0
- package/lib/ui/KavchatChat.js +150 -0
- package/lib/ui/KavchatLauncher.d.ts +12 -0
- package/lib/ui/KavchatLauncher.js +44 -0
- package/lib/ui/KavchatProvider.d.ts +62 -0
- package/lib/ui/KavchatProvider.js +113 -0
- package/package.json +58 -4
- package/src/api.ts +167 -0
- package/src/controller.ts +512 -0
- package/src/index.ts +13 -0
- package/src/screens.ts +141 -0
- package/src/storage.ts +105 -0
- package/src/text.ts +84 -0
- package/src/theme.ts +69 -0
- package/src/ui/KavchatChat.tsx +383 -0
- package/src/ui/KavchatLauncher.tsx +81 -0
- package/src/ui/KavchatProvider.tsx +166 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Modal, Pressable, SafeAreaView, StyleSheet, View } from 'react-native';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import { KavchatChat } from './KavchatChat';
|
|
4
|
+
import { useKavchatContext } from './KavchatProvider';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* כפתור צף שפותח את הצ'אט בחלון מלא
|
|
8
|
+
* הכפתור לא מוצג כשהבוט לא זמין (כבוי, testers, מכסה), כמו בווידג'ט לאתר
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface KavchatLauncherProps {
|
|
12
|
+
/** מיקום הכפתור. ברירת מחדל: פינה תחתונה בצד הסוף. end מתהפך לבד ב-RTL */
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
/** בלי החלון המובנה, למשל כשהאפליקציה פותחת את KavchatChat במסך ניווט משלה */
|
|
15
|
+
onPress?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function KavchatLauncher({ style, onPress }: KavchatLauncherProps) {
|
|
19
|
+
const { snapshot, theme, isOpen, open, close } = useKavchatContext();
|
|
20
|
+
if (snapshot.status !== 'ready') return null;
|
|
21
|
+
const t = snapshot.texts;
|
|
22
|
+
return (
|
|
23
|
+
<>
|
|
24
|
+
<Pressable
|
|
25
|
+
onPress={onPress ?? open}
|
|
26
|
+
accessibilityRole="button"
|
|
27
|
+
accessibilityLabel={t['widget.open']}
|
|
28
|
+
style={({ pressed }) => [styles.fab, { backgroundColor: theme.primary }, pressed && styles.pressed, style]}
|
|
29
|
+
>
|
|
30
|
+
<ChatIcon color={theme.onPrimary} />
|
|
31
|
+
</Pressable>
|
|
32
|
+
{onPress ? null : (
|
|
33
|
+
<Modal visible={isOpen} animationType="slide" presentationStyle="pageSheet" onRequestClose={close}>
|
|
34
|
+
<SafeAreaView style={[styles.flex, { backgroundColor: theme.primary }]}>
|
|
35
|
+
<KavchatChat onClose={close} />
|
|
36
|
+
</SafeAreaView>
|
|
37
|
+
</Modal>
|
|
38
|
+
)}
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** בועת צ'אט מ-View בלבד, כדי לא לחייב את האפליקציה בספריית אייקונים */
|
|
44
|
+
function ChatIcon({ color }: { color: string }) {
|
|
45
|
+
return (
|
|
46
|
+
<View style={styles.icon}>
|
|
47
|
+
<View style={[styles.iconBody, { backgroundColor: color }]} />
|
|
48
|
+
<View style={[styles.iconTail, { borderTopColor: color }]} />
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const styles = StyleSheet.create({
|
|
54
|
+
flex: { flex: 1 },
|
|
55
|
+
fab: {
|
|
56
|
+
position: 'absolute',
|
|
57
|
+
bottom: 24,
|
|
58
|
+
end: 20,
|
|
59
|
+
width: 56,
|
|
60
|
+
height: 56,
|
|
61
|
+
borderRadius: 28,
|
|
62
|
+
alignItems: 'center',
|
|
63
|
+
justifyContent: 'center',
|
|
64
|
+
elevation: 6,
|
|
65
|
+
shadowColor: '#000',
|
|
66
|
+
shadowOpacity: 0.2,
|
|
67
|
+
shadowRadius: 6,
|
|
68
|
+
shadowOffset: { width: 0, height: 3 },
|
|
69
|
+
},
|
|
70
|
+
pressed: { opacity: 0.85 },
|
|
71
|
+
icon: { width: 24, height: 24 },
|
|
72
|
+
iconBody: { width: 24, height: 18, borderRadius: 5 },
|
|
73
|
+
iconTail: {
|
|
74
|
+
width: 0,
|
|
75
|
+
height: 0,
|
|
76
|
+
marginStart: 4,
|
|
77
|
+
borderTopWidth: 6,
|
|
78
|
+
borderEndWidth: 6,
|
|
79
|
+
borderEndColor: 'transparent',
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
2
|
+
import { AppState, Linking } from 'react-native';
|
|
3
|
+
import type { FetchLike } from '../api';
|
|
4
|
+
import { KavchatController, KavchatEvent, KavchatSnapshot } from '../controller';
|
|
5
|
+
import type { KavchatScreen } from '../screens';
|
|
6
|
+
import { StorageAdapter, defaultStorage } from '../storage';
|
|
7
|
+
import { deviceLocale } from '../text';
|
|
8
|
+
import { KavchatTheme, themeFor } from '../theme';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ה-Provider מחזיק בקר אחד לכל האפליקציה, כדי שהשיחה תישמר גם כשמסך הצ'אט נסגר ונפתח
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface KavchatProviderProps {
|
|
15
|
+
/** המפתח הציבורי pk_live_... */
|
|
16
|
+
publicKey: string;
|
|
17
|
+
/** ברירת מחדל https://api.kavchat.com */
|
|
18
|
+
apiBase?: string;
|
|
19
|
+
/** שפת הממשק. ברירת מחדל: שפת המכשיר, ואם אין לה תרגום, שפת הבוט */
|
|
20
|
+
locale?: string;
|
|
21
|
+
/** JWT לזיהוי המשתמש, נחתם בשרת של הלקוח עם המפתח הסודי. לעולם לא המפתח הסודי עצמו */
|
|
22
|
+
userToken?: string | null;
|
|
23
|
+
userName?: string | null;
|
|
24
|
+
/** כפתור מסוג screen בתסריט: ניווט למסך באפליקציה */
|
|
25
|
+
onScreen?: (screen: string, targetId?: string) => void;
|
|
26
|
+
/** מסכי האפליקציה. נרשמים בשרת, והעורך בלוח הניהול מציע אותם ברשימה */
|
|
27
|
+
screens?: readonly KavchatScreen[];
|
|
28
|
+
onEvent?: (event: KavchatEvent) => void;
|
|
29
|
+
/** ברירת מחדל: AsyncStorage אם מותקן, אחרת זיכרון בלבד */
|
|
30
|
+
storage?: StorageAdapter;
|
|
31
|
+
/** עקיפה של הצבעים שמגיעים מההגדרות בלוח הניהול */
|
|
32
|
+
theme?: Partial<KavchatTheme>;
|
|
33
|
+
/** להחלפה בבדיקות */
|
|
34
|
+
fetch?: FetchLike;
|
|
35
|
+
children?: React.ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface KavchatContextValue {
|
|
39
|
+
controller: KavchatController;
|
|
40
|
+
snapshot: KavchatSnapshot;
|
|
41
|
+
theme: KavchatTheme;
|
|
42
|
+
isOpen: boolean;
|
|
43
|
+
open: () => void;
|
|
44
|
+
close: () => void;
|
|
45
|
+
toggle: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KavchatContext = createContext<KavchatContextValue | null>(null);
|
|
49
|
+
|
|
50
|
+
export function KavchatProvider(props: KavchatProviderProps) {
|
|
51
|
+
const { publicKey, apiBase, locale, userToken, userName, onScreen, screens, onEvent, storage, theme, fetch, children } = props;
|
|
52
|
+
const controllerRef = useRef<KavchatController | null>(null);
|
|
53
|
+
const keyRef = useRef('');
|
|
54
|
+
const key = `${publicKey}|${apiBase ?? ''}`;
|
|
55
|
+
if (!controllerRef.current || keyRef.current !== key) {
|
|
56
|
+
controllerRef.current?.dispose();
|
|
57
|
+
keyRef.current = key;
|
|
58
|
+
controllerRef.current = new KavchatController({
|
|
59
|
+
publicKey,
|
|
60
|
+
apiBase,
|
|
61
|
+
locale,
|
|
62
|
+
userToken: userToken || undefined,
|
|
63
|
+
userName: userName || undefined,
|
|
64
|
+
storage: storage ?? defaultStorage(),
|
|
65
|
+
deviceLocale: deviceLocale(),
|
|
66
|
+
screens,
|
|
67
|
+
fetch,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const controller = controllerRef.current;
|
|
71
|
+
|
|
72
|
+
// callbacks חדשים בכל רינדור לא בונים בקר חדש
|
|
73
|
+
controller.setCallbacks({
|
|
74
|
+
onScreen,
|
|
75
|
+
onEvent,
|
|
76
|
+
onLink: (url) => {
|
|
77
|
+
Linking.openURL(url).catch((e: unknown) => console.warn('Kavchat: cannot open link', e));
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
void controller.load();
|
|
83
|
+
// לא dispose: ב-StrictMode האפקט רץ פעמיים על אותו בקר. רק שולחים את מה שנצבר
|
|
84
|
+
return () => {
|
|
85
|
+
void controller.flush();
|
|
86
|
+
};
|
|
87
|
+
}, [controller]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
void controller.setUser({ token: userToken, name: userName });
|
|
91
|
+
}, [controller, userToken, userName]);
|
|
92
|
+
|
|
93
|
+
// מערך חדש בכל רינדור לא שולח שוב: משווים לפי התוכן
|
|
94
|
+
const screensKey = JSON.stringify(screens ?? null);
|
|
95
|
+
const firstScreens = useRef(true);
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
// בבנייה הבקר כבר קיבל את המסכים
|
|
98
|
+
if (firstScreens.current) {
|
|
99
|
+
firstScreens.current = false;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
controller.registerScreens(screens);
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, [controller, screensKey]);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
controller.setLocale(locale);
|
|
108
|
+
}, [controller, locale]);
|
|
109
|
+
|
|
110
|
+
// דיווח המסלול כשהאפליקציה עוברת לרקע, כי ייתכן שלא תחזור
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const sub = AppState.addEventListener('change', (s) => {
|
|
113
|
+
if (s === 'background' || s === 'inactive') void controller.flush();
|
|
114
|
+
});
|
|
115
|
+
return () => sub.remove();
|
|
116
|
+
}, [controller]);
|
|
117
|
+
|
|
118
|
+
const snapshot = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
119
|
+
const [isOpen, setOpen] = useState(false);
|
|
120
|
+
const open = useCallback(() => {
|
|
121
|
+
controller.ensureConversation();
|
|
122
|
+
setOpen(true);
|
|
123
|
+
}, [controller]);
|
|
124
|
+
const close = useCallback(() => setOpen(false), []);
|
|
125
|
+
const toggle = useCallback(() => (isOpen ? close() : open()), [isOpen, open, close]);
|
|
126
|
+
|
|
127
|
+
const primary = snapshot.release?.settings.primaryColor;
|
|
128
|
+
const themeKey = JSON.stringify(theme ?? null);
|
|
129
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
|
+
const resolved = useMemo(() => themeFor(primary, theme), [primary, themeKey]);
|
|
131
|
+
|
|
132
|
+
const value = useMemo<KavchatContextValue>(
|
|
133
|
+
() => ({ controller, snapshot, theme: resolved, isOpen: isOpen && snapshot.status === 'ready', open, close, toggle }),
|
|
134
|
+
[controller, snapshot, resolved, isOpen, open, close, toggle],
|
|
135
|
+
);
|
|
136
|
+
return <KavchatContext.Provider value={value}>{children}</KavchatContext.Provider>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function useKavchatContext(): KavchatContextValue {
|
|
140
|
+
const ctx = useContext(KavchatContext);
|
|
141
|
+
if (!ctx) throw new Error('Kavchat: wrap your app with <KavchatProvider>');
|
|
142
|
+
return ctx;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** ה-hook הציבורי: מצב הפתיחה, זמינות הבוט והחלפת משתמש */
|
|
146
|
+
export function useKavchat() {
|
|
147
|
+
const { controller, snapshot, isOpen, open, close, toggle } = useKavchatContext();
|
|
148
|
+
return {
|
|
149
|
+
status: snapshot.status,
|
|
150
|
+
/** false כשהבוט כבוי, מוגבל למשתמשי בדיקה או שנגמרה המכסה. אז לא מציגים כניסה לצ'אט */
|
|
151
|
+
available: snapshot.status === 'ready',
|
|
152
|
+
offline: snapshot.offline,
|
|
153
|
+
locale: snapshot.locale,
|
|
154
|
+
isOpen,
|
|
155
|
+
open,
|
|
156
|
+
close,
|
|
157
|
+
toggle,
|
|
158
|
+
setUser: (user: { token?: string | null; name?: string | null } | null) => controller.setUser(user),
|
|
159
|
+
startNewConversation: () => controller.startFresh(),
|
|
160
|
+
openContactForm: (prefill?: string) => {
|
|
161
|
+
open();
|
|
162
|
+
controller.openContactForm(prefill);
|
|
163
|
+
},
|
|
164
|
+
controller,
|
|
165
|
+
};
|
|
166
|
+
}
|