@nativetalkcommunications/react-native-call-sdk 0.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/NativetalkCallSdk.podspec +31 -0
  3. package/README.md +494 -0
  4. package/android/build.gradle +58 -0
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/AndroidManifest.xml +84 -0
  7. package/android/src/main/java/io/nativetalk/callsdk/BackgroundService.kt +149 -0
  8. package/android/src/main/java/io/nativetalk/callsdk/CallActionReceiver.kt +24 -0
  9. package/android/src/main/java/io/nativetalk/callsdk/CallService.kt +45 -0
  10. package/android/src/main/java/io/nativetalk/callsdk/Compatibility.kt +96 -0
  11. package/android/src/main/java/io/nativetalk/callsdk/CoreManager.kt +801 -0
  12. package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallScreeningService.kt +105 -0
  13. package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallSdkModule.kt +205 -0
  14. package/android/src/main/java/io/nativetalk/callsdk/NativetalkCallSdkPackage.kt +18 -0
  15. package/android/src/main/java/io/nativetalk/callsdk/TelephonyMonitor.kt +229 -0
  16. package/android/src/main/java/io/nativetalk/callsdk/Utils.kt +42 -0
  17. package/android/src/main/res/drawable/ic_nativetalk_call.xml +9 -0
  18. package/android/src/main/res/values/strings.xml +9 -0
  19. package/app.plugin.js +1 -0
  20. package/ios/NativetalkCallSdk-Bridging-Header.h +4 -0
  21. package/ios/NativetalkCallSdk.swift +738 -0
  22. package/ios/NativetalkCallSdkBridge.m +35 -0
  23. package/lib/commonjs/CallProvider.js +602 -0
  24. package/lib/commonjs/helpers.js +173 -0
  25. package/lib/commonjs/index.js +96 -0
  26. package/lib/commonjs/native.js +146 -0
  27. package/lib/commonjs/types.js +8 -0
  28. package/lib/commonjs/ui/Avatar.js +29 -0
  29. package/lib/commonjs/ui/Dialer.js +189 -0
  30. package/lib/commonjs/ui/IncomingCallView.js +128 -0
  31. package/lib/commonjs/ui/OutgoingCallView.js +117 -0
  32. package/lib/commonjs/ui/index.js +22 -0
  33. package/lib/commonjs/ui/theme.js +21 -0
  34. package/lib/module/CallProvider.js +573 -0
  35. package/lib/module/helpers.js +161 -0
  36. package/lib/module/index.js +57 -0
  37. package/lib/module/native.js +123 -0
  38. package/lib/module/types.js +7 -0
  39. package/lib/module/ui/Avatar.js +22 -0
  40. package/lib/module/ui/Dialer.js +162 -0
  41. package/lib/module/ui/IncomingCallView.js +101 -0
  42. package/lib/module/ui/OutgoingCallView.js +110 -0
  43. package/lib/module/ui/index.js +13 -0
  44. package/lib/module/ui/theme.js +17 -0
  45. package/lib/typescript/CallProvider.d.ts +46 -0
  46. package/lib/typescript/helpers.d.ts +52 -0
  47. package/lib/typescript/index.d.ts +77 -0
  48. package/lib/typescript/native.d.ts +53 -0
  49. package/lib/typescript/types.d.ts +155 -0
  50. package/lib/typescript/ui/Avatar.d.ts +13 -0
  51. package/lib/typescript/ui/Dialer.d.ts +29 -0
  52. package/lib/typescript/ui/IncomingCallView.d.ts +39 -0
  53. package/lib/typescript/ui/OutgoingCallView.d.ts +28 -0
  54. package/lib/typescript/ui/index.d.ts +13 -0
  55. package/lib/typescript/ui/theme.d.ts +20 -0
  56. package/linphonesw-pod/Sources/LinphoneSdkInfos.swift +4 -0
  57. package/linphonesw-pod/Sources/LinphoneWrapper.swift +42949 -0
  58. package/linphonesw-pod/linphonesw.podspec +46 -0
  59. package/package.json +90 -0
  60. package/plugin/build/index.js +12 -0
  61. package/plugin/build/withAndroid.js +78 -0
  62. package/plugin/build/withIos.js +66 -0
  63. package/src/CallProvider.tsx +675 -0
  64. package/src/helpers.ts +179 -0
  65. package/src/index.ts +84 -0
  66. package/src/native.ts +185 -0
  67. package/src/types.ts +202 -0
  68. package/src/ui/Avatar.tsx +46 -0
  69. package/src/ui/Dialer.tsx +248 -0
  70. package/src/ui/IncomingCallView.tsx +161 -0
  71. package/src/ui/OutgoingCallView.tsx +203 -0
  72. package/src/ui/index.ts +13 -0
  73. package/src/ui/theme.ts +36 -0
  74. package/ui/package.json +6 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Bundled dial-pad component.
3
+ *
4
+ * Drop it anywhere inside a `<CallProvider>` and it'll dial through the SDK.
5
+ * Override almost everything via props if you want to keep the layout but
6
+ * change the look.
7
+ */
8
+ import React, { useState } from 'react';
9
+ import {
10
+ ScrollView,
11
+ StyleSheet,
12
+ Text,
13
+ TextInput,
14
+ TouchableOpacity,
15
+ View,
16
+ useWindowDimensions,
17
+ } from 'react-native';
18
+
19
+ import { useCall } from '../CallProvider';
20
+ import { sanitizeDial } from '../helpers';
21
+ import { mergeTheme, type CallTheme } from './theme';
22
+
23
+ interface DialerProps {
24
+ /** Initial value of the input. */
25
+ initialValue?: string;
26
+ /** Called whenever the user submits a number (presses the call button or hits return). */
27
+ onDialed?: (number: string) => void;
28
+ /** Header rendered above the input. Pass `null` to hide. */
29
+ header?: React.ReactNode | null;
30
+ /** Custom call button. Receives an `onPress` and the current number. */
31
+ renderCallButton?: (props: {
32
+ onPress: () => void;
33
+ disabled: boolean;
34
+ number: string;
35
+ }) => React.ReactNode;
36
+ /** Theme overrides. */
37
+ theme?: Partial<CallTheme>;
38
+ /** If true (default) the dial-pad plays a DTMF UI tone on each key press. */
39
+ playKeyTones?: boolean;
40
+ }
41
+
42
+ const dialPad: Array<Array<{ number: string; letters: string }>> = [
43
+ [
44
+ { number: '1', letters: '' },
45
+ { number: '2', letters: 'ABC' },
46
+ { number: '3', letters: 'DEF' },
47
+ ],
48
+ [
49
+ { number: '4', letters: 'GHI' },
50
+ { number: '5', letters: 'JKL' },
51
+ { number: '6', letters: 'MNO' },
52
+ ],
53
+ [
54
+ { number: '7', letters: 'PQRS' },
55
+ { number: '8', letters: 'TUV' },
56
+ { number: '9', letters: 'WXYZ' },
57
+ ],
58
+ [
59
+ { number: '*', letters: '' },
60
+ { number: '0', letters: '+' },
61
+ { number: '#', letters: '' },
62
+ ],
63
+ ];
64
+
65
+ export function Dialer({
66
+ initialValue = '',
67
+ onDialed,
68
+ header,
69
+ renderCallButton,
70
+ theme,
71
+ playKeyTones = true,
72
+ }: DialerProps) {
73
+ const [input, setInput] = useState(initialValue);
74
+ const { dial, playKeyTone } = useCall();
75
+ const { width, height } = useWindowDimensions();
76
+ const isCompact = height < 760;
77
+ const buttonSize = Math.min(width / 4.3, isCompact ? 68 : 84);
78
+ const t = mergeTheme(theme);
79
+
80
+ const handlePress = (value: string) => {
81
+ if (playKeyTones) playKeyTone(value);
82
+ setInput((prev) => prev + value);
83
+ };
84
+
85
+ const handleBackspace = () => setInput((prev) => prev.slice(0, -1));
86
+
87
+ const handleCall = async () => {
88
+ if (!input) return;
89
+ try {
90
+ await dial(input);
91
+ onDialed?.(input);
92
+ } catch (err: any) {
93
+ // Errors surface via <CallProvider onError>. Swallow here so the dialer
94
+ // doesn't crash the host app.
95
+ }
96
+ };
97
+
98
+ const callDisabled = input.length === 0;
99
+
100
+ return (
101
+ <View style={[styles.container, { backgroundColor: t.background }]}>
102
+ {header}
103
+ <View style={styles.inputContainer}>
104
+ <TextInput
105
+ value={input}
106
+ placeholder="Enter Number"
107
+ placeholderTextColor="#ccc"
108
+ onChangeText={(s) => setInput(sanitizeDial(s))}
109
+ keyboardType="phone-pad"
110
+ inputMode="tel"
111
+ autoCorrect={false}
112
+ autoCapitalize="none"
113
+ maxLength={64}
114
+ style={[styles.inputText, { color: t.text }]}
115
+ returnKeyType="done"
116
+ onSubmitEditing={handleCall}
117
+ />
118
+ {input.length > 0 && (
119
+ <TouchableOpacity style={styles.clearButton} onPress={handleBackspace}>
120
+ <Text style={{ fontSize: 22, color: '#999' }}>⌫</Text>
121
+ </TouchableOpacity>
122
+ )}
123
+ </View>
124
+
125
+ <ScrollView
126
+ style={styles.padContainer}
127
+ contentContainerStyle={[
128
+ styles.padContent,
129
+ { paddingTop: isCompact ? 16 : 30, paddingBottom: 24 },
130
+ ]}
131
+ bounces={false}
132
+ showsVerticalScrollIndicator={false}
133
+ >
134
+ {dialPad.map((row, i) => (
135
+ <View
136
+ style={[styles.padRow, { marginBottom: isCompact ? 8 : 10 }]}
137
+ key={i}
138
+ >
139
+ {row.map((item) => (
140
+ <TouchableOpacity
141
+ key={item.number}
142
+ style={[
143
+ styles.padButton,
144
+ {
145
+ width: buttonSize,
146
+ height: buttonSize,
147
+ borderRadius: buttonSize / 2,
148
+ marginHorizontal: isCompact ? 6 : 8,
149
+ },
150
+ ]}
151
+ onPress={() => handlePress(item.number)}
152
+ activeOpacity={0.8}
153
+ >
154
+ <Text
155
+ style={[
156
+ styles.padNumber,
157
+ { fontSize: isCompact ? 28 : 30, color: t.text },
158
+ ]}
159
+ >
160
+ {item.number}
161
+ </Text>
162
+ {!!item.letters && (
163
+ <Text style={[styles.padLetters, { color: t.subtext }]}>
164
+ {item.letters}
165
+ </Text>
166
+ )}
167
+ </TouchableOpacity>
168
+ ))}
169
+ </View>
170
+ ))}
171
+
172
+ <View style={{ marginTop: isCompact ? 10 : 15 }}>
173
+ {renderCallButton ? (
174
+ renderCallButton({
175
+ onPress: handleCall,
176
+ disabled: callDisabled,
177
+ number: input,
178
+ })
179
+ ) : (
180
+ <TouchableOpacity
181
+ onPress={handleCall}
182
+ disabled={callDisabled}
183
+ style={[
184
+ styles.callButton,
185
+ {
186
+ backgroundColor: callDisabled ? '#ccc' : t.answer,
187
+ width: isCompact ? 62 : 70,
188
+ height: isCompact ? 62 : 70,
189
+ borderRadius: (isCompact ? 62 : 70) / 2,
190
+ },
191
+ ]}
192
+ >
193
+ <Text style={{ color: '#fff', fontSize: 28 }}>📞</Text>
194
+ </TouchableOpacity>
195
+ )}
196
+ </View>
197
+ </ScrollView>
198
+ </View>
199
+ );
200
+ }
201
+
202
+ const styles = StyleSheet.create({
203
+ container: { flex: 1 },
204
+ inputContainer: {
205
+ flexDirection: 'row',
206
+ alignItems: 'center',
207
+ justifyContent: 'center',
208
+ minHeight: 100,
209
+ marginTop: 10,
210
+ marginBottom: 10,
211
+ paddingHorizontal: 30,
212
+ position: 'relative',
213
+ },
214
+ inputText: {
215
+ flex: 1,
216
+ fontSize: 26,
217
+ textAlign: 'center',
218
+ fontWeight: '600',
219
+ letterSpacing: 2,
220
+ paddingVertical: 12,
221
+ },
222
+ clearButton: {
223
+ position: 'absolute',
224
+ right: 35,
225
+ padding: 6,
226
+ zIndex: 10,
227
+ },
228
+ padContainer: {
229
+ flex: 1,
230
+ borderTopLeftRadius: 22,
231
+ borderTopRightRadius: 22,
232
+ },
233
+ padContent: { alignItems: 'center' },
234
+ padRow: { flexDirection: 'row', justifyContent: 'center' },
235
+ padButton: {
236
+ backgroundColor: '#fff',
237
+ justifyContent: 'center',
238
+ alignItems: 'center',
239
+ elevation: 1,
240
+ shadowColor: '#000',
241
+ shadowOffset: { width: 0, height: 1 },
242
+ shadowOpacity: 0.07,
243
+ shadowRadius: 1.5,
244
+ },
245
+ padNumber: { fontWeight: '600', textAlign: 'center' },
246
+ padLetters: { fontSize: 11, letterSpacing: 2, textAlign: 'center', marginTop: 2 },
247
+ callButton: { alignItems: 'center', justifyContent: 'center' },
248
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Drop-in screen rendered when an incoming call is ringing.
3
+ *
4
+ * Wire-up:
5
+ *
6
+ * ```tsx
7
+ * <CallProvider
8
+ * onIncomingCall={() => navigation.navigate('IncomingCall')}
9
+ * config={cfg}
10
+ * >
11
+ * …
12
+ * </CallProvider>
13
+ *
14
+ * // your IncomingCall screen:
15
+ * <IncomingCallView onAnswered={() => navigation.replace('InCall')} />
16
+ * ```
17
+ */
18
+ import React, { useEffect } from 'react';
19
+ import {
20
+ StyleSheet,
21
+ Text,
22
+ TouchableOpacity,
23
+ View,
24
+ type StyleProp,
25
+ type ViewStyle,
26
+ } from 'react-native';
27
+
28
+ import { useCall } from '../CallProvider';
29
+ import { parseSipUser } from '../helpers';
30
+ import { Avatar } from './Avatar';
31
+ import { mergeTheme, type CallTheme } from './theme';
32
+
33
+ interface IncomingCallViewProps {
34
+ /** Called once `answer()` resolves — typically you navigate to the in-call screen here. */
35
+ onAnswered?: () => void;
36
+ /** Called after `decline()` completes. */
37
+ onDeclined?: () => void;
38
+ /** Called when there's no live incoming call (e.g. caller hung up first). */
39
+ onDismissed?: () => void;
40
+ /** Override the location string under the caller name. */
41
+ location?: string;
42
+ /** Custom title — defaults to "Incoming call". */
43
+ title?: string;
44
+ /** Theme overrides. */
45
+ theme?: Partial<CallTheme>;
46
+ /** Optional top-of-screen element (e.g. a logo). */
47
+ header?: React.ReactNode;
48
+ style?: StyleProp<ViewStyle>;
49
+ }
50
+
51
+ export function IncomingCallView({
52
+ onAnswered,
53
+ onDeclined,
54
+ onDismissed,
55
+ location,
56
+ title = 'Incoming call',
57
+ theme,
58
+ header,
59
+ style,
60
+ }: IncomingCallViewProps) {
61
+ const { incoming, incomingInfo, answer, decline } = useCall();
62
+ const t = mergeTheme(theme);
63
+
64
+ // Auto-dismiss if the call ends while this screen is mounted.
65
+ useEffect(() => {
66
+ if (!incoming) onDismissed?.();
67
+ }, [incoming, onDismissed]);
68
+
69
+ const pretty = (s = '') =>
70
+ s.includes('@') ? parseSipUser(s) : s;
71
+
72
+ const name = incomingInfo?.name ?? 'Unknown';
73
+ const phone = incomingInfo?.phone ?? '';
74
+ const initials = incomingInfo?.initials ?? '??';
75
+
76
+ const onAnswer = async () => {
77
+ await answer();
78
+ onAnswered?.();
79
+ };
80
+
81
+ const onDecline = async () => {
82
+ await decline('busy');
83
+ onDeclined?.();
84
+ };
85
+
86
+ return (
87
+ <View style={[styles.container, { backgroundColor: t.background }, style]}>
88
+ {header}
89
+ <Text style={[styles.status, { color: t.text }]}>{title}</Text>
90
+
91
+ <View style={styles.avatarWrap}>
92
+ <Avatar
93
+ initials={initials}
94
+ size={80}
95
+ color={t.primary}
96
+ background="#EEF2FF"
97
+ />
98
+ </View>
99
+
100
+ <Text style={[styles.name, { color: t.text }]}>{pretty(name)}</Text>
101
+ <Text style={[styles.phone, { color: t.text }]}>{pretty(phone)}</Text>
102
+ {!!location && (
103
+ <Text style={[styles.location, { color: t.subtext }]}>{location}</Text>
104
+ )}
105
+
106
+ <View style={styles.bottomRow}>
107
+ <TouchableOpacity
108
+ onPress={onDecline}
109
+ activeOpacity={0.85}
110
+ style={[styles.circleBtn, { backgroundColor: t.decline }]}
111
+ >
112
+ <Text style={styles.circleIcon}>✕</Text>
113
+ </TouchableOpacity>
114
+
115
+ <TouchableOpacity
116
+ onPress={onAnswer}
117
+ activeOpacity={0.85}
118
+ style={[styles.circleBtn, { backgroundColor: t.answer }]}
119
+ >
120
+ <Text style={styles.circleIcon}>📞</Text>
121
+ </TouchableOpacity>
122
+ </View>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ container: {
129
+ flex: 1,
130
+ alignItems: 'center',
131
+ justifyContent: 'flex-start',
132
+ paddingVertical: 60,
133
+ },
134
+ status: { fontSize: 18, marginBottom: 16, marginTop: 8 },
135
+ avatarWrap: { marginBottom: 22, marginTop: 6 },
136
+ name: { fontSize: 28, fontWeight: '800', textAlign: 'center' },
137
+ phone: { fontSize: 18, marginTop: 8 },
138
+ location: { fontSize: 16, marginTop: 8 },
139
+ bottomRow: {
140
+ position: 'absolute',
141
+ bottom: 60,
142
+ left: 0,
143
+ right: 0,
144
+ flexDirection: 'row',
145
+ justifyContent: 'space-between',
146
+ paddingHorizontal: 40,
147
+ },
148
+ circleBtn: {
149
+ width: 80,
150
+ height: 80,
151
+ borderRadius: 40,
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ shadowColor: '#000',
155
+ shadowOpacity: 0.15,
156
+ shadowOffset: { width: 0, height: 4 },
157
+ shadowRadius: 8,
158
+ elevation: 4,
159
+ },
160
+ circleIcon: { fontSize: 32, color: '#fff' },
161
+ });
@@ -0,0 +1,203 @@
1
+ /**
2
+ * In-call screen used for both outgoing calls and after answering an incoming
3
+ * call. Shows the caller, status, timer, and mute/speaker/end controls.
4
+ *
5
+ * Pure presentation — all state comes from `useCall()`.
6
+ */
7
+ import React from 'react';
8
+ import {
9
+ StyleSheet,
10
+ Text,
11
+ TouchableOpacity,
12
+ View,
13
+ type StyleProp,
14
+ type ViewStyle,
15
+ } from 'react-native';
16
+
17
+ import { useCall } from '../CallProvider';
18
+ import { callStatusLabel, parseSipUser } from '../helpers';
19
+ import { Avatar } from './Avatar';
20
+ import { mergeTheme, type CallTheme } from './theme';
21
+
22
+ interface OutgoingCallViewProps {
23
+ /** Caller name to display. Falls back to the SIP user-part. */
24
+ name?: string;
25
+ /** Phone number to display under the name. */
26
+ phone?: string;
27
+ /** Free-form location string (e.g. "Lagos, Nigeria"). Optional. */
28
+ location?: string;
29
+ /** Two-character avatar initials. */
30
+ initials?: string;
31
+ /** Called after hangup completes. Usually you `navigation.goBack()`. */
32
+ onEnded?: () => void;
33
+ /** Theme overrides. */
34
+ theme?: Partial<CallTheme>;
35
+ /** Optional top-of-screen element (e.g. a logo). */
36
+ header?: React.ReactNode;
37
+ style?: StyleProp<ViewStyle>;
38
+ }
39
+
40
+ export function OutgoingCallView({
41
+ name,
42
+ phone,
43
+ location,
44
+ initials,
45
+ onEnded,
46
+ theme,
47
+ header,
48
+ style,
49
+ }: OutgoingCallViewProps) {
50
+ const {
51
+ callStatus,
52
+ hangup,
53
+ toggleMute,
54
+ toggleSpeaker,
55
+ formattedDuration,
56
+ isMuted,
57
+ isSpeaker,
58
+ } = useCall();
59
+ const t = mergeTheme(theme);
60
+
61
+ const status = callStatusLabel(callStatus);
62
+ const showTimer = ['In progress', 'On hold', 'Call ended'].includes(status);
63
+ const ended = status === 'Call ended';
64
+
65
+ const pretty = (s = '') => (s.includes('@') ? parseSipUser(s) : s);
66
+
67
+ const handleEnd = async () => {
68
+ if (!ended) await hangup();
69
+ onEnded?.();
70
+ };
71
+
72
+ return (
73
+ <View style={[styles.container, { backgroundColor: t.background }, style]}>
74
+ {header}
75
+ <Text style={[styles.status, ended && { color: t.decline }, !ended && { color: t.text }]}>
76
+ {status}
77
+ </Text>
78
+ {showTimer && (
79
+ <Text style={[styles.duration, { color: t.text }]}>{formattedDuration}</Text>
80
+ )}
81
+
82
+ <View style={styles.avatarWrap}>
83
+ <Avatar
84
+ initials={initials ?? (name || '??').slice(0, 2)}
85
+ size={80}
86
+ color={t.primary}
87
+ background="#EEF2FF"
88
+ />
89
+ </View>
90
+
91
+ {!!name && <Text style={[styles.name, { color: t.text }]}>{pretty(name)}</Text>}
92
+ {!!phone && <Text style={[styles.phone, { color: t.text }]}>{pretty(phone)}</Text>}
93
+ {!!location && (
94
+ <Text style={[styles.location, { color: t.subtext }]}>{location}</Text>
95
+ )}
96
+
97
+ <View style={styles.controlsGrid}>
98
+ <CallControl
99
+ label={isMuted ? 'Unmute' : 'Mute'}
100
+ icon="🎙"
101
+ active={isMuted}
102
+ disabled={ended}
103
+ onPress={toggleMute}
104
+ theme={t}
105
+ />
106
+ <CallControl
107
+ label={isSpeaker ? 'Earpiece' : 'Speaker'}
108
+ icon="🔊"
109
+ active={isSpeaker}
110
+ disabled={ended}
111
+ onPress={toggleSpeaker}
112
+ theme={t}
113
+ />
114
+ <View style={styles.controlPlaceholder} />
115
+ </View>
116
+
117
+ <TouchableOpacity
118
+ style={[styles.endBtn, { backgroundColor: t.decline }]}
119
+ onPress={handleEnd}
120
+ >
121
+ <Text style={{ color: '#fff', fontSize: 28 }}>✕</Text>
122
+ </TouchableOpacity>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ interface ControlProps {
128
+ label: string;
129
+ icon: string;
130
+ active?: boolean;
131
+ disabled?: boolean;
132
+ onPress: () => void;
133
+ theme: CallTheme;
134
+ }
135
+
136
+ function CallControl({ label, icon, active, disabled, onPress, theme }: ControlProps) {
137
+ const bg = active ? theme.controlOnBg : theme.controlOffBg;
138
+ const iconColor = active
139
+ ? theme.controlIconOn
140
+ : disabled
141
+ ? '#ccc'
142
+ : theme.controlIconOff;
143
+ return (
144
+ <TouchableOpacity
145
+ style={[styles.control, disabled && styles.controlDisabled]}
146
+ onPress={onPress}
147
+ disabled={disabled}
148
+ activeOpacity={0.7}
149
+ >
150
+ <View style={[styles.controlCircle, { backgroundColor: bg }]}>
151
+ <Text style={{ fontSize: 24, color: iconColor }}>{icon}</Text>
152
+ </View>
153
+ <Text style={[styles.controlLabel, disabled && { color: '#aaa' }]}>
154
+ {label}
155
+ </Text>
156
+ </TouchableOpacity>
157
+ );
158
+ }
159
+
160
+ const styles = StyleSheet.create({
161
+ container: {
162
+ flex: 1,
163
+ alignItems: 'center',
164
+ justifyContent: 'flex-start',
165
+ paddingVertical: 60,
166
+ },
167
+ status: { fontSize: 15, marginBottom: 6, marginTop: 10 },
168
+ duration: { fontSize: 16, fontWeight: '600', marginBottom: 10 },
169
+ avatarWrap: { marginBottom: 10, marginTop: 15 },
170
+ name: { fontSize: 22, fontWeight: '700', textAlign: 'center' },
171
+ phone: { fontSize: 18, marginTop: 6, marginBottom: 6 },
172
+ location: { fontSize: 15, marginBottom: 16 },
173
+ controlsGrid: {
174
+ width: '88%',
175
+ flexDirection: 'row',
176
+ flexWrap: 'wrap',
177
+ justifyContent: 'space-between',
178
+ alignSelf: 'center',
179
+ marginTop: 30,
180
+ marginBottom: 18,
181
+ },
182
+ control: { width: '30%', alignItems: 'center', marginVertical: 16 },
183
+ controlPlaceholder: { width: '30%' },
184
+ controlDisabled: { opacity: 0.48 },
185
+ controlCircle: {
186
+ width: 65,
187
+ height: 65,
188
+ borderRadius: 50,
189
+ alignItems: 'center',
190
+ justifyContent: 'center',
191
+ marginBottom: 7,
192
+ },
193
+ controlLabel: { fontSize: 15, color: '#222', textAlign: 'center' },
194
+ endBtn: {
195
+ position: 'absolute',
196
+ bottom: 60,
197
+ width: 70,
198
+ height: 70,
199
+ borderRadius: 35,
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ },
203
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Optional UI components for the Nativetalk Call SDK.
3
+ *
4
+ * These are entirely opt-in — the core hook (`useCall()`) gives you everything
5
+ * you need to roll your own UI. The components here are kept deliberately
6
+ * minimal (no external icon packs, no navigation library) so they work in
7
+ * any RN project.
8
+ */
9
+ export { Dialer } from './Dialer';
10
+ export { IncomingCallView } from './IncomingCallView';
11
+ export { OutgoingCallView } from './OutgoingCallView';
12
+ export { Avatar } from './Avatar';
13
+ export { defaultTheme, mergeTheme, type CallTheme } from './theme';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Default look-and-feel for the bundled UI components.
3
+ *
4
+ * Override per-component via the `theme` prop, or wrap the SDK components in
5
+ * your own and ignore these entirely.
6
+ */
7
+ export interface CallTheme {
8
+ primary: string;
9
+ background: string;
10
+ text: string;
11
+ subtext: string;
12
+ answer: string;
13
+ decline: string;
14
+ controlOnBg: string;
15
+ controlOffBg: string;
16
+ controlIconOn: string;
17
+ controlIconOff: string;
18
+ }
19
+
20
+ export const defaultTheme: CallTheme = {
21
+ primary: '#2D6BFF',
22
+ background: '#fafafa',
23
+ text: '#111',
24
+ subtext: '#555',
25
+ answer: '#33c124',
26
+ decline: '#ff2d2d',
27
+ controlOnBg: '#111',
28
+ controlOffBg: '#fff',
29
+ controlIconOn: '#fff',
30
+ controlIconOff: '#111',
31
+ };
32
+
33
+ export function mergeTheme(override?: Partial<CallTheme>): CallTheme {
34
+ if (!override) return defaultTheme;
35
+ return { ...defaultTheme, ...override };
36
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "main": "../lib/commonjs/ui/index.js",
3
+ "module": "../lib/module/ui/index.js",
4
+ "types": "../lib/typescript/ui/index.d.ts",
5
+ "react-native": "../src/ui/index.ts"
6
+ }