shakebug-react-native 2.4.14 → 2.4.16

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 (32) hide show
  1. package/README.md +6 -0
  2. package/android/src/main/java/com/shakebug/ConnectivityReceiver.java +12 -7
  3. package/android/src/main/java/com/shakebug/DrawingView.java +13 -3
  4. package/android/src/main/java/com/shakebug/RecordScreenModule.java +6 -3
  5. package/android/src/main/java/com/shakebug/ViewShot.java +1 -0
  6. package/ios/CustomScreenShot/ShakebugSDKCustomScreenCapture.m +6 -2
  7. package/ios/ScreenShot/ShakebugCapture.m +3 -1
  8. package/ios/SketchCanvas/DrawingView.m +14 -1
  9. package/package.json +1 -1
  10. package/shakebug-react-native-2.4.15.tgz +0 -0
  11. package/src/APIs/MatchKey.js +1 -1
  12. package/src/Assets/images/shake.png +0 -0
  13. package/src/Assets/images/undo.png +0 -0
  14. package/src/Assets/images/undo@2x.png +0 -0
  15. package/src/Assets/images/undo@3x.png +0 -0
  16. package/src/Componanats/CustomAlert.js +8 -4
  17. package/src/Componanats/Icons.js +278 -0
  18. package/src/Componanats/KeyboardAvoidingWrapper.js +31 -39
  19. package/src/Componanats/LoadingModal.js +7 -4
  20. package/src/Componanats/NavigationContext.js +8 -1
  21. package/src/Componanats/ReviewModal.js +18 -11
  22. package/src/Componanats/SafeAreaHelper.js +70 -0
  23. package/src/Componanats/ShakeFunction.js +655 -364
  24. package/src/Componanats/ShakebugBottomSheet.js +729 -0
  25. package/src/Componanats/Theme.js +96 -0
  26. package/src/Componanats/ZoomableImage.js +350 -0
  27. package/src/Componanats/localization.js +111 -82
  28. package/src/Screens/AddDetailsScreen.js +800 -439
  29. package/src/Screens/FirstScreen.js +950 -540
  30. package/src/Screens/Shakebug.js +78 -44
  31. package/src/Screens/TrimVideoScreen.js +40 -7
  32. package/src/index.tsx +16 -7
@@ -0,0 +1,96 @@
1
+ import { useColorScheme, Appearance } from 'react-native';
2
+
3
+ /**
4
+ * Determines whether Shakebug should render in dark mode.
5
+ * Priority:
6
+ * 1. Explicit developer override (global.ShakebugTheme / global.ShakebugDarkMode)
7
+ * 2. Automatically inferred from application / device theme (useColorScheme / Appearance)
8
+ */
9
+ export const isShakebugDark = (colorScheme) => {
10
+ if (global.ShakebugTheme === 'dark' || global.ShakebugDarkMode === true) {
11
+ return true;
12
+ }
13
+ if (global.ShakebugTheme === 'light' || global.ShakebugDarkMode === false) {
14
+ return false;
15
+ }
16
+
17
+ // Automatically take theme from the application / system
18
+ const scheme = (colorScheme !== undefined && colorScheme !== null)
19
+ ? colorScheme
20
+ : (Appearance && Appearance.getColorScheme ? Appearance.getColorScheme() : null);
21
+
22
+ return scheme === 'dark';
23
+ };
24
+
25
+ export const useTheme = (customHeaderColor) => {
26
+ // Automatically detects and follows application / system theme
27
+ const colorScheme = useColorScheme();
28
+ const isDark = isShakebugDark(colorScheme);
29
+
30
+ // Always respect the given Shakebug theme color, falling back to default
31
+ const brandColor = customHeaderColor || '#2E2E2E';
32
+
33
+ return {
34
+ isDark,
35
+ theme: isDark ? 'dark' : 'light',
36
+
37
+ // Primary Brand / Header Color
38
+ brandColor,
39
+ headerBg: brandColor,
40
+ headerText: '#ffffff',
41
+
42
+ // Screen backgrounds (clean white in light mode, dark charcoal in dark mode)
43
+ backgroundColor: isDark ? '#21252C' : '#ffffff',
44
+ detailsBg: isDark ? '#21252C' : '#ffffff',
45
+
46
+ // Card and surface backgrounds
47
+ cardBg: isDark ? '#21252C' : '#ffffff',
48
+ surfaceBg: isDark ? '#21252C' : '#ffffff',
49
+
50
+ // Bottom Sheet background
51
+ sheetBg: isDark ? '#313A46' : '#ffffff',
52
+ sheetHandle: isDark ? '#313A46' : '#d1d5db',
53
+
54
+ // Text colors
55
+ textColor: isDark ? '#ffffff' : '#0f172a',
56
+ textSecondary: isDark ? '#8e9db0' : '#64748b',
57
+ titleColor: isDark ? '#ffffff' : '#0f172a',
58
+
59
+ // Borders
60
+ borderColor: isDark ? '#2c323d' : '#e2e8f0',
61
+ inputBorder: isDark ? '#363b46' : '#e2e8f0',
62
+
63
+ // Inputs
64
+ inputBg: isDark ? '#2b2f38' : '#ffffff',
65
+ placeholderColor: isDark ? '#8e9db0' : '#94a3b8',
66
+
67
+ // Action buttons (Replace Image / Replace Video)
68
+ buttonBg: isDark ? '#363a44' : '#edf2f7',
69
+ buttonBorder: isDark ? '#444955' : '#cbd5e1',
70
+ buttonText: isDark ? '#8e9db0' : '#64748b',
71
+
72
+ // Send button in header
73
+ sendButtonBg: '#ffffff',
74
+ sendButtonText: brandColor,
75
+
76
+ // Bottom sheet buttons
77
+ sheetPrimaryBtnBg: brandColor,
78
+ sheetPrimaryBtnText: '#ffffff',
79
+ sheetButtonBg: isDark ? '#313A46' : '#ffffff',
80
+ sheetButtonBorder: isDark ? '#444b57' : '#e2e8f0',
81
+ sheetButtonText: isDark ? '#ebeef1' : '#0f172a',
82
+
83
+ // Floating annotation bar (always dark floating pill for contrast against screenshots)
84
+ floatingBarBg: '#242b35',
85
+ floatingBarBorder: 'transparent',
86
+
87
+ // Confirmation Modal
88
+ confirmModalBg: isDark ? '#2b2f38' : '#ffffff',
89
+ confirmModalText: isDark ? '#ffffff' : '#111827',
90
+ confirmButtonBg: isDark ? '#3a404c' : '#e2e8f0',
91
+ confirmCancelText: isDark ? '#ffffff' : '#111827',
92
+ confirmDisableText: '#ef4444',
93
+ };
94
+ };
95
+
96
+ export default useTheme;
@@ -0,0 +1,350 @@
1
+ import React, { useRef, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ Image,
5
+ PanResponder,
6
+ Animated,
7
+ StyleSheet,
8
+ Dimensions,
9
+ Platform,
10
+ } from 'react-native';
11
+
12
+ const ZoomableImage = ({ uri, style, resizeMode = 'contain' }) => {
13
+ const scale = useRef(new Animated.Value(1)).current;
14
+ const pan = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current;
15
+
16
+ // Track values synchronously for smooth gesture calculations
17
+ const currentScaleRef = useRef(1);
18
+ const currentPanRef = useRef({ x: 0, y: 0 });
19
+
20
+ const isPinchingRef = useRef(false);
21
+ const isPanningRef = useRef(false);
22
+ const initialDistanceRef = useRef(0);
23
+ const baseScaleRef = useRef(1);
24
+ const initialCenterRef = useRef({ x: 0, y: 0 });
25
+ const basePanRef = useRef({ x: 0, y: 0 });
26
+ const panStartTouchRef = useRef({ x: 0, y: 0 });
27
+ const lastTapRef = useRef(0);
28
+
29
+ const windowDim = Dimensions.get('window');
30
+ const layoutSizeRef = useRef({
31
+ width: windowDim.width,
32
+ height: 300,
33
+ });
34
+
35
+ useEffect(() => {
36
+ const scaleId = scale.addListener(({ value }) => {
37
+ currentScaleRef.current = value;
38
+ });
39
+ const panId = pan.addListener(({ x, y }) => {
40
+ currentPanRef.current = { x, y };
41
+ });
42
+ return () => {
43
+ if (typeof scale.removeListener === 'function') {
44
+ scale.removeListener(scaleId);
45
+ }
46
+ if (typeof pan.removeListener === 'function') {
47
+ pan.removeListener(panId);
48
+ }
49
+ };
50
+ }, [scale, pan]);
51
+
52
+ const onLayout = (e) => {
53
+ const { width, height } = e.nativeEvent.layout;
54
+ if (width > 0 && height > 0) {
55
+ layoutSizeRef.current = { width, height };
56
+ }
57
+ };
58
+
59
+ const getDistance = (touches) => {
60
+ if (!touches || touches.length < 2) return 0;
61
+ const dx = touches[0].pageX - touches[1].pageX;
62
+ const dy = touches[0].pageY - touches[1].pageY;
63
+ return Math.sqrt(dx * dx + dy * dy);
64
+ };
65
+
66
+ const getCenter = (touches) => {
67
+ if (!touches || touches.length < 2) return { x: 0, y: 0 };
68
+ return {
69
+ x: (touches[0].pageX + touches[1].pageX) / 2,
70
+ y: (touches[0].pageY + touches[1].pageY) / 2,
71
+ };
72
+ };
73
+
74
+ const handleRelease = () => {
75
+ isPinchingRef.current = false;
76
+ isPanningRef.current = false;
77
+ initialDistanceRef.current = 0;
78
+
79
+ if (currentScaleRef.current < 1.1) {
80
+ // Smoothly spring back to 1x and center (0, 0)
81
+ Animated.parallel([
82
+ Animated.spring(scale, {
83
+ toValue: 1,
84
+ friction: 7,
85
+ tension: 50,
86
+ useNativeDriver: true,
87
+ }),
88
+ Animated.spring(pan, {
89
+ toValue: { x: 0, y: 0 },
90
+ friction: 7,
91
+ tension: 50,
92
+ useNativeDriver: true,
93
+ }),
94
+ ]).start();
95
+ currentScaleRef.current = 1;
96
+ currentPanRef.current = { x: 0, y: 0 };
97
+ } else {
98
+ // Clamping within bounds
99
+ const { width, height } = layoutSizeRef.current;
100
+ const effectiveScale = Math.min(currentScaleRef.current, 4.5);
101
+ const maxX = Math.max(0, ((effectiveScale - 1) * width) / 2);
102
+ const maxY = Math.max(0, ((effectiveScale - 1) * height) / 2);
103
+
104
+ const boundedX = Math.max(-maxX, Math.min(maxX, currentPanRef.current.x));
105
+ const boundedY = Math.max(-maxY, Math.min(maxY, currentPanRef.current.y));
106
+
107
+ const animations = [];
108
+
109
+ if (currentScaleRef.current > 4.5) {
110
+ animations.push(
111
+ Animated.spring(scale, {
112
+ toValue: 4.0,
113
+ friction: 7,
114
+ tension: 50,
115
+ useNativeDriver: true,
116
+ })
117
+ );
118
+ currentScaleRef.current = 4.0;
119
+ }
120
+
121
+ if (
122
+ Math.abs(boundedX - currentPanRef.current.x) > 0.5 ||
123
+ Math.abs(boundedY - currentPanRef.current.y) > 0.5
124
+ ) {
125
+ animations.push(
126
+ Animated.spring(pan, {
127
+ toValue: { x: boundedX, y: boundedY },
128
+ friction: 7,
129
+ tension: 50,
130
+ useNativeDriver: true,
131
+ })
132
+ );
133
+ currentPanRef.current = { x: boundedX, y: boundedY };
134
+ }
135
+
136
+ if (animations.length > 0) {
137
+ Animated.parallel(animations).start();
138
+ }
139
+ }
140
+ };
141
+
142
+ const panResponder = useRef(
143
+ PanResponder.create({
144
+ onStartShouldSetPanResponder: () => true,
145
+ onStartShouldSetPanResponderCapture: (evt) => {
146
+ return evt.nativeEvent.touches && evt.nativeEvent.touches.length > 1;
147
+ },
148
+ onMoveShouldSetPanResponder: (evt, gestureState) => {
149
+ const touches = evt.nativeEvent.touches;
150
+ return (
151
+ (touches && touches.length > 1) ||
152
+ (currentScaleRef.current > 1.05 &&
153
+ (Math.abs(gestureState.dx) > 2 || Math.abs(gestureState.dy) > 2))
154
+ );
155
+ },
156
+ onMoveShouldSetPanResponderCapture: (evt) => {
157
+ return evt.nativeEvent.touches && evt.nativeEvent.touches.length > 1;
158
+ },
159
+ onPanResponderTerminationRequest: (evt) => {
160
+ // Never allow parent ScrollViews to steal active multi-touch pinch or zoomed-in pan
161
+ if (
162
+ (evt.nativeEvent.touches && evt.nativeEvent.touches.length > 1) ||
163
+ currentScaleRef.current > 1.05
164
+ ) {
165
+ return false;
166
+ }
167
+ return true;
168
+ },
169
+ onShouldBlockNativeResponder: () => true,
170
+
171
+ onPanResponderGrant: (evt) => {
172
+ const touches = evt.nativeEvent.touches;
173
+ const now = Date.now();
174
+
175
+ if (touches && touches.length >= 2) {
176
+ isPinchingRef.current = true;
177
+ isPanningRef.current = false;
178
+ initialDistanceRef.current = getDistance(touches);
179
+ baseScaleRef.current = currentScaleRef.current;
180
+ initialCenterRef.current = getCenter(touches);
181
+ basePanRef.current = { ...currentPanRef.current };
182
+ } else {
183
+ isPinchingRef.current = false;
184
+ isPanningRef.current = true;
185
+ if (touches && touches.length === 1) {
186
+ panStartTouchRef.current = {
187
+ x: touches[0].pageX,
188
+ y: touches[0].pageY,
189
+ };
190
+ }
191
+ basePanRef.current = { ...currentPanRef.current };
192
+
193
+ // Double tap toggle: 1x <-> 2.2x
194
+ if (now - lastTapRef.current < 300) {
195
+ lastTapRef.current = 0;
196
+ if (currentScaleRef.current > 1.2) {
197
+ Animated.parallel([
198
+ Animated.spring(scale, {
199
+ toValue: 1,
200
+ friction: 7,
201
+ tension: 50,
202
+ useNativeDriver: true,
203
+ }),
204
+ Animated.spring(pan, {
205
+ toValue: { x: 0, y: 0 },
206
+ friction: 7,
207
+ tension: 50,
208
+ useNativeDriver: true,
209
+ }),
210
+ ]).start();
211
+ currentScaleRef.current = 1;
212
+ currentPanRef.current = { x: 0, y: 0 };
213
+ } else {
214
+ Animated.spring(scale, {
215
+ toValue: 2.2,
216
+ friction: 7,
217
+ tension: 50,
218
+ useNativeDriver: true,
219
+ }).start();
220
+ currentScaleRef.current = 2.2;
221
+ }
222
+ } else {
223
+ lastTapRef.current = now;
224
+ }
225
+ }
226
+ },
227
+
228
+ onPanResponderMove: (evt) => {
229
+ const touches = evt.nativeEvent.touches;
230
+ if (touches && touches.length >= 2) {
231
+ const currentDist = getDistance(touches);
232
+ const currentCenter = getCenter(touches);
233
+
234
+ if (!isPinchingRef.current || !initialDistanceRef.current) {
235
+ isPinchingRef.current = true;
236
+ isPanningRef.current = false;
237
+ initialDistanceRef.current = currentDist;
238
+ baseScaleRef.current = currentScaleRef.current;
239
+ initialCenterRef.current = currentCenter;
240
+ basePanRef.current = { ...currentPanRef.current };
241
+ return;
242
+ }
243
+
244
+ if (initialDistanceRef.current > 0) {
245
+ const factor = currentDist / initialDistanceRef.current;
246
+ const targetScale = Math.max(
247
+ 0.85,
248
+ Math.min(5.0, baseScaleRef.current * factor)
249
+ );
250
+ scale.setValue(targetScale);
251
+ currentScaleRef.current = targetScale;
252
+
253
+ const deltaX = currentCenter.x - initialCenterRef.current.x;
254
+ const deltaY = currentCenter.y - initialCenterRef.current.y;
255
+ const targetPanX = basePanRef.current.x + deltaX;
256
+ const targetPanY = basePanRef.current.y + deltaY;
257
+ pan.setValue({ x: targetPanX, y: targetPanY });
258
+ currentPanRef.current = { x: targetPanX, y: targetPanY };
259
+ }
260
+ } else if (touches && touches.length === 1) {
261
+ if (isPinchingRef.current) {
262
+ isPinchingRef.current = false;
263
+ isPanningRef.current = true;
264
+ initialDistanceRef.current = 0;
265
+ panStartTouchRef.current = {
266
+ x: touches[0].pageX,
267
+ y: touches[0].pageY,
268
+ };
269
+ basePanRef.current = { ...currentPanRef.current };
270
+ } else if (isPanningRef.current && currentScaleRef.current > 1.05) {
271
+ const deltaX = touches[0].pageX - panStartTouchRef.current.x;
272
+ const deltaY = touches[0].pageY - panStartTouchRef.current.y;
273
+ const targetPanX = basePanRef.current.x + deltaX;
274
+ const targetPanY = basePanRef.current.y + deltaY;
275
+ pan.setValue({ x: targetPanX, y: targetPanY });
276
+ currentPanRef.current = { x: targetPanX, y: targetPanY };
277
+ }
278
+ }
279
+ },
280
+
281
+ onPanResponderRelease: handleRelease,
282
+ onPanResponderTerminate: handleRelease,
283
+ })
284
+ ).current;
285
+
286
+ return (
287
+ <View
288
+ style={[styles.container, style]}
289
+ onLayout={onLayout}
290
+ collapsable={false}
291
+ >
292
+ <Animated.View
293
+ {...panResponder.panHandlers}
294
+ style={[
295
+ styles.imageWrapper,
296
+ {
297
+ transform: [
298
+ { translateX: pan.x },
299
+ { translateY: pan.y },
300
+ { scale },
301
+ ],
302
+ },
303
+ ]}
304
+ collapsable={false}
305
+ >
306
+ {uri ? (
307
+ <Image
308
+ source={{
309
+ uri:
310
+ Platform.OS === 'android' &&
311
+ typeof uri === 'string' &&
312
+ !uri.startsWith('file://') &&
313
+ !uri.startsWith('content://') &&
314
+ !uri.startsWith('http')
315
+ ? `file://${uri}`
316
+ : uri,
317
+ }}
318
+ style={styles.image}
319
+ resizeMode={resizeMode}
320
+ />
321
+ ) : null}
322
+ </Animated.View>
323
+ </View>
324
+ );
325
+ };
326
+
327
+ const styles = StyleSheet.create({
328
+ container: {
329
+ flex: 1,
330
+ width: '100%',
331
+ height: '100%',
332
+ overflow: 'hidden',
333
+ justifyContent: 'center',
334
+ alignItems: 'center',
335
+ backgroundColor: 'transparent',
336
+ },
337
+ imageWrapper: {
338
+ width: '100%',
339
+ height: '100%',
340
+ justifyContent: 'center',
341
+ alignItems: 'center',
342
+ backgroundColor: 'transparent',
343
+ },
344
+ image: {
345
+ width: '100%',
346
+ height: '100%',
347
+ },
348
+ });
349
+
350
+ export default ZoomableImage;