@zezosoft/zezo-ott-react-native-ui-kit 1.1.2 → 1.1.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.
Files changed (136) hide show
  1. package/lib/module/components/Auth/QrLogin/QrLogin.js +304 -138
  2. package/lib/module/components/Auth/QrLogin/QrLogin.js.map +1 -1
  3. package/lib/module/components/Auth/QrLogin/components/QrViewArea.js +193 -141
  4. package/lib/module/components/Auth/QrLogin/components/QrViewArea.js.map +1 -1
  5. package/lib/module/components/Content/Card/Category/Category.js +83 -11
  6. package/lib/module/components/Content/Card/Category/Category.js.map +1 -1
  7. package/lib/module/components/Content/Card/NowWatching/NowWatching.js +237 -108
  8. package/lib/module/components/Content/Card/NowWatching/NowWatching.js.map +1 -1
  9. package/lib/module/components/Content/Card/Sliders/Styles/One.js +185 -126
  10. package/lib/module/components/Content/Card/Sliders/Styles/One.js.map +1 -1
  11. package/lib/module/components/Content/Card/Sliders/Styles/Two.js +139 -92
  12. package/lib/module/components/Content/Card/Sliders/Styles/Two.js.map +1 -1
  13. package/lib/module/components/Content/Card/Styles/Five.js +131 -48
  14. package/lib/module/components/Content/Card/Styles/Five.js.map +1 -1
  15. package/lib/module/components/Content/Card/Styles/Four.js +126 -59
  16. package/lib/module/components/Content/Card/Styles/Four.js.map +1 -1
  17. package/lib/module/components/Content/Card/Styles/One.js +125 -50
  18. package/lib/module/components/Content/Card/Styles/One.js.map +1 -1
  19. package/lib/module/components/Content/Card/Styles/RotateInOut.js +138 -53
  20. package/lib/module/components/Content/Card/Styles/RotateInOut.js.map +1 -1
  21. package/lib/module/components/Content/Card/Styles/Six.js +207 -115
  22. package/lib/module/components/Content/Card/Styles/Six.js.map +1 -1
  23. package/lib/module/components/Content/Card/Styles/Three.js +134 -79
  24. package/lib/module/components/Content/Card/Styles/Three.js.map +1 -1
  25. package/lib/module/components/Content/Card/Styles/TopTen.js +186 -171
  26. package/lib/module/components/Content/Card/Styles/TopTen.js.map +1 -1
  27. package/lib/module/components/Content/Card/Styles/Two.js +144 -64
  28. package/lib/module/components/Content/Card/Styles/Two.js.map +1 -1
  29. package/lib/module/components/Content/Card/components/AdsPoster.js +162 -0
  30. package/lib/module/components/Content/Card/components/AdsPoster.js.map +1 -0
  31. package/lib/module/components/Content/Card/components/CardPoster.js +120 -136
  32. package/lib/module/components/Content/Card/components/CardPoster.js.map +1 -1
  33. package/lib/module/components/Content/Card/components/index.js +4 -0
  34. package/lib/module/components/Content/Card/components/index.js.map +1 -0
  35. package/lib/module/components/Content/Content.js +67 -27
  36. package/lib/module/components/Content/Content.js.map +1 -1
  37. package/lib/module/components/Content/Sections.js +32 -11
  38. package/lib/module/components/Content/Sections.js.map +1 -1
  39. package/lib/module/constants/dummySections.js +44 -4
  40. package/lib/module/constants/dummySections.js.map +1 -1
  41. package/lib/module/hooks/Images/index.js +5 -0
  42. package/lib/module/hooks/Images/index.js.map +1 -0
  43. package/lib/module/hooks/Images/useImageLoader.js +168 -0
  44. package/lib/module/hooks/Images/useImageLoader.js.map +1 -0
  45. package/lib/module/hooks/Images/useImageValidation.js +36 -0
  46. package/lib/module/hooks/Images/useImageValidation.js.map +1 -0
  47. package/lib/module/hooks/index.js +3 -0
  48. package/lib/module/hooks/index.js.map +1 -1
  49. package/lib/module/hooks/useAdTracking.js +270 -0
  50. package/lib/module/hooks/useAdTracking.js.map +1 -0
  51. package/lib/module/hooks/useCards.js +164 -0
  52. package/lib/module/hooks/useCards.js.map +1 -0
  53. package/lib/module/hooks/usePaginatedSection.js +11 -6
  54. package/lib/module/hooks/usePaginatedSection.js.map +1 -1
  55. package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts +2 -0
  56. package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Auth/QrLogin/components/QrViewArea.d.ts.map +1 -1
  58. package/lib/typescript/src/components/Content/Card/Category/Category.d.ts.map +1 -1
  59. package/lib/typescript/src/components/Content/Card/NowWatching/NowWatching.d.ts.map +1 -1
  60. package/lib/typescript/src/components/Content/Card/Sliders/Styles/One.d.ts.map +1 -1
  61. package/lib/typescript/src/components/Content/Card/Sliders/Styles/Two.d.ts.map +1 -1
  62. package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts +13 -1
  63. package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts.map +1 -1
  64. package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts +13 -1
  65. package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts.map +1 -1
  66. package/lib/typescript/src/components/Content/Card/Styles/One.d.ts +15 -3
  67. package/lib/typescript/src/components/Content/Card/Styles/One.d.ts.map +1 -1
  68. package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts +13 -1
  69. package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts.map +1 -1
  70. package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts +1 -0
  71. package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts.map +1 -1
  72. package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts +13 -5
  73. package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts.map +1 -1
  74. package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts +1 -0
  75. package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts.map +1 -1
  76. package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts +13 -1
  77. package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts.map +1 -1
  78. package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts +26 -0
  79. package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts.map +1 -0
  80. package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts +3 -1
  81. package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts.map +1 -1
  82. package/lib/typescript/src/components/Content/Card/components/index.d.ts +2 -0
  83. package/lib/typescript/src/components/Content/Card/components/index.d.ts.map +1 -0
  84. package/lib/typescript/src/components/Content/Card/index.d.ts +76 -6
  85. package/lib/typescript/src/components/Content/Card/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/Content/Content.d.ts +4 -3
  87. package/lib/typescript/src/components/Content/Content.d.ts.map +1 -1
  88. package/lib/typescript/src/components/Content/Sections.d.ts +20 -6
  89. package/lib/typescript/src/components/Content/Sections.d.ts.map +1 -1
  90. package/lib/typescript/src/constants/dummySections.d.ts +5 -0
  91. package/lib/typescript/src/constants/dummySections.d.ts.map +1 -1
  92. package/lib/typescript/src/hooks/Images/index.d.ts +3 -0
  93. package/lib/typescript/src/hooks/Images/index.d.ts.map +1 -0
  94. package/lib/typescript/src/hooks/Images/useImageLoader.d.ts +36 -0
  95. package/lib/typescript/src/hooks/Images/useImageLoader.d.ts.map +1 -0
  96. package/lib/typescript/src/hooks/Images/useImageValidation.d.ts +17 -0
  97. package/lib/typescript/src/hooks/Images/useImageValidation.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/index.d.ts +3 -0
  99. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  100. package/lib/typescript/src/hooks/useAdTracking.d.ts +39 -0
  101. package/lib/typescript/src/hooks/useAdTracking.d.ts.map +1 -0
  102. package/lib/typescript/src/hooks/useCards.d.ts +36 -0
  103. package/lib/typescript/src/hooks/useCards.d.ts.map +1 -0
  104. package/lib/typescript/src/hooks/usePaginatedSection.d.ts +12 -2
  105. package/lib/typescript/src/hooks/usePaginatedSection.d.ts.map +1 -1
  106. package/lib/typescript/src/types/sections/index.d.ts +7 -4
  107. package/lib/typescript/src/types/sections/index.d.ts.map +1 -1
  108. package/package.json +6 -3
  109. package/src/components/Auth/QrLogin/QrLogin.tsx +382 -122
  110. package/src/components/Auth/QrLogin/components/QrViewArea.tsx +291 -197
  111. package/src/components/Content/Card/Category/Category.tsx +95 -8
  112. package/src/components/Content/Card/NowWatching/NowWatching.tsx +281 -136
  113. package/src/components/Content/Card/Sliders/Styles/One.tsx +244 -148
  114. package/src/components/Content/Card/Sliders/Styles/Two.tsx +171 -102
  115. package/src/components/Content/Card/Styles/Five.tsx +161 -62
  116. package/src/components/Content/Card/Styles/Four.tsx +164 -85
  117. package/src/components/Content/Card/Styles/One.tsx +161 -71
  118. package/src/components/Content/Card/Styles/RotateInOut.tsx +157 -60
  119. package/src/components/Content/Card/Styles/Six.tsx +242 -142
  120. package/src/components/Content/Card/Styles/Three.tsx +166 -133
  121. package/src/components/Content/Card/Styles/TopTen.tsx +230 -191
  122. package/src/components/Content/Card/Styles/Two.tsx +182 -79
  123. package/src/components/Content/Card/components/AdsPoster.tsx +202 -0
  124. package/src/components/Content/Card/components/CardPoster.tsx +134 -154
  125. package/src/components/Content/Card/components/index.ts +1 -0
  126. package/src/components/Content/Content.tsx +83 -45
  127. package/src/components/Content/Sections.tsx +51 -10
  128. package/src/constants/dummySections.ts +48 -1
  129. package/src/hooks/Images/index.ts +2 -0
  130. package/src/hooks/Images/useImageLoader.ts +206 -0
  131. package/src/hooks/Images/useImageValidation.ts +36 -0
  132. package/src/hooks/index.ts +3 -0
  133. package/src/hooks/useAdTracking.ts +349 -0
  134. package/src/hooks/useCards.ts +228 -0
  135. package/src/hooks/usePaginatedSection.ts +26 -7
  136. package/src/types/sections/index.ts +7 -4
@@ -2,7 +2,13 @@
2
2
  * @author Ashok Desai
3
3
  * @lastModified oct 02 Oct 2025 at 10:30 AM
4
4
  */
5
- import React, { useState, useRef, useMemo, useCallback } from 'react';
5
+ import React, {
6
+ useState,
7
+ useRef,
8
+ useMemo,
9
+ useCallback,
10
+ useEffect,
11
+ } from 'react';
6
12
  import {
7
13
  View,
8
14
  StyleSheet,
@@ -10,8 +16,10 @@ import {
10
16
  TouchableOpacity,
11
17
  ActivityIndicator,
12
18
  } from 'react-native';
19
+ import type { ViewStyle, TextStyle } from 'react-native';
13
20
  import { scale } from 'react-native-size-matters';
14
21
  import { RFValue } from 'react-native-responsive-fontsize';
22
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
15
23
  import { Text } from '../../Text';
16
24
  import AppHeader from '../../Headers/AppHeader';
17
25
  import { useInternalTheme } from '../../../theme/hook/useInternalTheme';
@@ -28,28 +36,265 @@ import QrViewArea from './components/QrViewArea';
28
36
  import { AppStatusBar } from '../../common/AppStatusBar';
29
37
 
30
38
  const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
31
- const HEADER_HEIGHT = 25;
32
-
33
- // QR Detection constants
34
- const SCAN_DELAY_MS = 1000; // Delay before triggering scan (reduced for faster response)
35
- const RESET_DELAY_MS = 1000; // Delay before resetting scan state (reduced for faster response)
36
39
 
40
+ // ==================== Constants ====================
41
+ const SCAN_DELAY_MS = 1000;
42
+ const RESET_DELAY_MS = 2000;
43
+ const CAMERA_TYPE = 'back' as const;
44
+ const CARD_WIDTH_OFFSET = 40;
45
+ const CARD_MIN_HEIGHT_RATIO = 2.5;
46
+ const ICON_SIZES = {
47
+ camera: 50,
48
+ qrCode: 180,
49
+ button: 22,
50
+ } as const;
51
+ const STROKE_WIDTHS = {
52
+ icon: 1.6,
53
+ button: 2,
54
+ } as const;
55
+
56
+ // ==================== Types ====================
37
57
  export type QrLoginProps = {
38
58
  title?: string;
39
59
  description?: string;
40
60
  scanButtonText?: string;
41
61
  onBackPress?: () => void;
42
62
  onScanSuccess?: (value: string) => void;
63
+ onScanError?: (error: string) => void;
64
+ allowMultipleScans?: boolean;
43
65
  theme?: ThemeOverride;
44
66
  };
45
67
 
68
+ // ==================== Default Values ====================
46
69
  const DEFAULTS = {
47
70
  title: 'QR Login',
48
71
  description: 'Please move your camera over the QR Code',
49
72
  scanButtonText: 'Scan QR Code',
50
- cameraType: 'back' as const,
73
+ cameraType: CAMERA_TYPE,
74
+ } as const;
75
+
76
+ // ==================== Helper Functions ====================
77
+ /**
78
+ * Extracts error message from error object
79
+ */
80
+ const getErrorMessage = (error: unknown, defaultMessage: string): string => {
81
+ return error instanceof Error ? error.message : defaultMessage;
51
82
  };
52
83
 
84
+ /**
85
+ * Creates style with background color
86
+ */
87
+ const createBackgroundStyle = (
88
+ baseStyle: ViewStyle,
89
+ backgroundColor: string
90
+ ): ViewStyle[] => [baseStyle, { backgroundColor }];
91
+
92
+ /**
93
+ * Creates text style with color
94
+ */
95
+ const createTextStyle = (baseStyle: TextStyle, color: string): TextStyle[] => [
96
+ baseStyle,
97
+ { color },
98
+ ];
99
+
100
+ // ==================== Custom Hooks ====================
101
+ /**
102
+ * Hook to manage QR scan state and timers
103
+ */
104
+ const useQrScanState = () => {
105
+ const scanTimer = useRef<NodeJS.Timeout | null>(null);
106
+ const resetTimer = useRef<NodeJS.Timeout | null>(null);
107
+ const lastValueRef = useRef<string | null>(null);
108
+ const isScanning = useRef(false);
109
+
110
+ const resetScanState = useCallback(() => {
111
+ if (scanTimer.current) {
112
+ clearTimeout(scanTimer.current);
113
+ scanTimer.current = null;
114
+ }
115
+ if (resetTimer.current) {
116
+ clearTimeout(resetTimer.current);
117
+ resetTimer.current = null;
118
+ }
119
+ isScanning.current = false;
120
+ lastValueRef.current = null;
121
+ }, []);
122
+
123
+ useEffect(() => {
124
+ return () => {
125
+ resetScanState();
126
+ };
127
+ }, [resetScanState]);
128
+
129
+ return {
130
+ scanTimer,
131
+ resetTimer,
132
+ lastValueRef,
133
+ isScanning,
134
+ resetScanState,
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Hook to handle camera permission request
140
+ */
141
+ const useCameraPermissionHandler = (
142
+ hasPermission: boolean,
143
+ requestPermission: () => Promise<boolean>,
144
+ onError?: (error: string) => void
145
+ ) => {
146
+ const [requesting, setRequesting] = useState(false);
147
+
148
+ const requestCameraPermission = useCallback(async (): Promise<boolean> => {
149
+ if (hasPermission) return true;
150
+
151
+ setRequesting(true);
152
+ try {
153
+ const granted = await requestPermission();
154
+ setRequesting(false);
155
+
156
+ if (!granted) {
157
+ onError?.('Camera permission denied');
158
+ return false;
159
+ }
160
+ return true;
161
+ } catch (error) {
162
+ setRequesting(false);
163
+ const errorMessage = getErrorMessage(
164
+ error,
165
+ 'Camera permission request failed'
166
+ );
167
+ console.warn('Camera permission request failed:', error);
168
+ onError?.(errorMessage);
169
+ return false;
170
+ }
171
+ }, [hasPermission, requestPermission, onError]);
172
+
173
+ return { requesting, requestCameraPermission };
174
+ };
175
+
176
+ // ==================== Sub-Components ====================
177
+ /**
178
+ * Camera view component when scanning is active
179
+ */
180
+ const CameraView: React.FC<{
181
+ device: ReturnType<typeof useCameraDevice>;
182
+ codeScanner: ReturnType<typeof useCodeScanner>;
183
+ theme?: ThemeOverride;
184
+ }> = React.memo(({ device, codeScanner, theme }) => {
185
+ if (!device) {
186
+ return null;
187
+ }
188
+
189
+ return (
190
+ <>
191
+ <Camera
192
+ style={StyleSheet.absoluteFill}
193
+ device={device}
194
+ isActive={true}
195
+ codeScanner={codeScanner}
196
+ enableFpsGraph={false}
197
+ />
198
+ <QrViewArea theme={theme} />
199
+ </>
200
+ );
201
+ });
202
+
203
+ CameraView.displayName = 'CameraView';
204
+
205
+ /**
206
+ * No camera available view
207
+ */
208
+ const NoCameraView: React.FC<{
209
+ textStyle: TextStyle;
210
+ containerStyle: ViewStyle[];
211
+ }> = React.memo(({ textStyle, containerStyle }) => (
212
+ <View style={containerStyle}>
213
+ <Text style={textStyle}>No camera available</Text>
214
+ </View>
215
+ ));
216
+
217
+ NoCameraView.displayName = 'NoCameraView';
218
+
219
+ /**
220
+ * Initial scan screen with QR code preview
221
+ */
222
+ const ScanPreviewScreen: React.FC<{
223
+ description: string;
224
+ scanButtonText: string;
225
+ requesting: boolean;
226
+ onStartScan: () => void;
227
+ colors: {
228
+ button: string;
229
+ textPrimary: string;
230
+ primary: string;
231
+ skeletonBaseColor: string;
232
+ };
233
+ styles: {
234
+ descText: TextStyle[];
235
+ qrBox: ViewStyle[];
236
+ scanBtn: ViewStyle[];
237
+ scanText: TextStyle[];
238
+ iconMargin: ViewStyle;
239
+ buttonIconMargin: ViewStyle;
240
+ loaderMargin: ViewStyle;
241
+ };
242
+ }> = React.memo(
243
+ ({
244
+ description,
245
+ scanButtonText,
246
+ requesting,
247
+ onStartScan,
248
+ colors,
249
+ styles: componentStyles,
250
+ }) => (
251
+ <View style={styles.content}>
252
+ <View style={styles.card}>
253
+ <CameraIcon
254
+ size={scale(ICON_SIZES.camera)}
255
+ color={colors.button}
256
+ strokeWidth={STROKE_WIDTHS.icon}
257
+ style={componentStyles.iconMargin}
258
+ />
259
+
260
+ <Text style={componentStyles.descText}>{description}</Text>
261
+
262
+ <View style={componentStyles.qrBox}>
263
+ <QrCode
264
+ size={scale(ICON_SIZES.qrCode)}
265
+ color={colors.button}
266
+ strokeWidth={STROKE_WIDTHS.icon}
267
+ />
268
+ </View>
269
+
270
+ <TouchableOpacity
271
+ style={componentStyles.scanBtn}
272
+ onPress={onStartScan}
273
+ activeOpacity={0.85}
274
+ >
275
+ <CameraIcon
276
+ size={scale(ICON_SIZES.button)}
277
+ color={colors.button}
278
+ strokeWidth={STROKE_WIDTHS.button}
279
+ style={componentStyles.buttonIconMargin}
280
+ />
281
+ <Text style={componentStyles.scanText}>{scanButtonText}</Text>
282
+ </TouchableOpacity>
283
+
284
+ {requesting && (
285
+ <ActivityIndicator
286
+ style={componentStyles.loaderMargin}
287
+ color={colors.primary}
288
+ />
289
+ )}
290
+ </View>
291
+ </View>
292
+ )
293
+ );
294
+
295
+ ScanPreviewScreen.displayName = 'ScanPreviewScreen';
296
+
297
+ // ==================== Main Component ====================
53
298
  export const QrLogin: React.FC<QrLoginProps> = React.memo(
54
299
  ({
55
300
  title = DEFAULTS.title,
@@ -57,119 +302,149 @@ export const QrLogin: React.FC<QrLoginProps> = React.memo(
57
302
  scanButtonText = DEFAULTS.scanButtonText,
58
303
  onBackPress,
59
304
  onScanSuccess,
305
+ onScanError,
306
+ allowMultipleScans = true,
60
307
  theme,
61
308
  }) => {
62
309
  const { theme: appliedTheme } = useInternalTheme(theme);
63
310
  const { colors } = appliedTheme;
311
+ const { top: statusBarHeight } = useSafeAreaInsets();
64
312
 
65
313
  const [scanActive, setScanActive] = useState(false);
66
- const [requesting, setRequesting] = useState(false);
67
314
 
68
315
  const device = useCameraDevice(DEFAULTS.cameraType);
69
316
  const { hasPermission, requestPermission } = useCameraPermission();
70
317
 
71
- const scanTimer = useRef<NodeJS.Timeout | null>(null);
72
- const lastValueRef = useRef<string | null>(null);
73
- const isScanning = useRef(false);
318
+ // QR scan state management
319
+ const { scanTimer, resetTimer, lastValueRef, isScanning, resetScanState } =
320
+ useQrScanState();
74
321
 
75
- // Cleanup timers on unmount
76
- React.useEffect(() => {
77
- return () => {
78
- if (scanTimer.current) {
79
- clearTimeout(scanTimer.current);
80
- }
81
- };
82
- }, []);
322
+ // Camera permission handler
323
+ const { requesting, requestCameraPermission } = useCameraPermissionHandler(
324
+ hasPermission,
325
+ requestPermission,
326
+ onScanError
327
+ );
328
+
329
+ // Stop scanning
330
+ const stopScan = useCallback(() => {
331
+ resetScanState();
332
+ setScanActive(false);
333
+ }, [resetScanState]);
334
+
335
+ // Handle back press
336
+ const handleBackPress = useCallback(() => {
337
+ if (scanActive) {
338
+ stopScan();
339
+ }
340
+ onBackPress?.();
341
+ }, [scanActive, stopScan, onBackPress]);
83
342
 
84
343
  // Handle code scanned
85
344
  const handleCodeScanned = useCallback(
86
345
  (codes: Code[]) => {
87
- // Skip if already processing a scan
88
346
  if (isScanning.current || codes.length === 0) return;
89
347
 
90
- // Find first valid QR code
91
348
  for (const code of codes) {
92
349
  const value = code?.value;
93
350
  if (!value) continue;
94
351
 
95
- // Skip if same code was just scanned
96
- if (lastValueRef.current === value) return;
352
+ // Prevent duplicate scans
353
+ if (!allowMultipleScans && lastValueRef.current === value) return;
97
354
 
98
- // Clear any existing timer
355
+ // Clear pending scan timer
99
356
  if (scanTimer.current) {
100
357
  clearTimeout(scanTimer.current);
358
+ scanTimer.current = null;
101
359
  }
102
360
 
103
- // Update last scanned value
104
361
  lastValueRef.current = value;
105
362
 
106
- // Delay scan trigger to avoid rapid multiple scans
363
+ // Trigger success callback after delay
107
364
  scanTimer.current = setTimeout(() => {
108
365
  isScanning.current = true;
109
- setScanActive(false);
110
- onScanSuccess?.(value);
111
-
112
- // Reset scan state after processing
113
- setTimeout(() => {
114
- isScanning.current = false;
115
- lastValueRef.current = null;
116
- }, RESET_DELAY_MS);
366
+ try {
367
+ onScanSuccess?.(value);
368
+ } catch (error) {
369
+ const errorMessage = getErrorMessage(
370
+ error,
371
+ 'Unknown error occurred'
372
+ );
373
+ onScanError?.(errorMessage);
374
+ console.error('QR scan success callback error:', error);
375
+ }
376
+
377
+ // Reset scan state after delay for re-scanning
378
+ if (allowMultipleScans) {
379
+ resetTimer.current = setTimeout(() => {
380
+ resetScanState();
381
+ }, RESET_DELAY_MS);
382
+ }
117
383
  }, SCAN_DELAY_MS);
118
384
 
119
- // Process only first valid code
120
385
  break;
121
386
  }
122
387
  },
123
- [onScanSuccess]
388
+ [
389
+ onScanSuccess,
390
+ onScanError,
391
+ allowMultipleScans,
392
+ resetScanState,
393
+ isScanning,
394
+ lastValueRef,
395
+ resetTimer,
396
+ scanTimer,
397
+ ]
124
398
  );
125
399
 
126
- // ---- Code Scanner ----
400
+ // Code scanner
127
401
  const codeScanner = useCodeScanner({
128
402
  codeTypes: ['qr'],
129
403
  onCodeScanned: handleCodeScanned,
130
404
  });
131
405
 
406
+ // Start scan
132
407
  const startScan = useCallback(async () => {
133
- if (!hasPermission) {
134
- setRequesting(true);
135
- try {
136
- const granted = await requestPermission();
137
- setRequesting(false);
138
- if (!granted) return;
139
- } catch (error) {
140
- setRequesting(false);
141
- console.warn('Camera permission request failed:', error);
142
- return;
143
- }
408
+ resetScanState();
409
+
410
+ const hasPermissionGranted = await requestCameraPermission();
411
+ if (!hasPermissionGranted) return;
412
+
413
+ if (!device) {
414
+ onScanError?.('No camera device available');
415
+ return;
144
416
  }
417
+
145
418
  setScanActive(true);
146
- }, [hasPermission, requestPermission]);
419
+ }, [device, resetScanState, requestCameraPermission, onScanError]);
147
420
 
148
- // Memoize container style
421
+ // Memoized styles
149
422
  const containerStyle = useMemo(
150
- () => [styles.container, { backgroundColor: colors.background }],
423
+ () => createBackgroundStyle(styles.container, colors.background),
151
424
  [colors.background]
152
425
  );
153
426
 
154
- // Memoize header wrapper style
155
427
  const headerWrapperStyle = useMemo(
156
- () => [styles.headerWrapper, { height: HEADER_HEIGHT }],
157
- []
428
+ () => [
429
+ styles.headerWrapper,
430
+ {
431
+ height: statusBarHeight,
432
+ backgroundColor: colors.background,
433
+ },
434
+ ],
435
+ [colors.background, statusBarHeight]
158
436
  );
159
437
 
160
- // Memoize center style
161
438
  const centerStyle = useMemo(
162
- () => [styles.center, { backgroundColor: colors.background }],
439
+ () => createBackgroundStyle(styles.center, colors.background),
163
440
  [colors.background]
164
441
  );
165
442
 
166
- // Memoize description text style
167
443
  const descTextStyle = useMemo(
168
- () => [styles.descText, { color: colors.textPrimary }],
444
+ () => createTextStyle(styles.descText, colors.textPrimary),
169
445
  [colors.textPrimary]
170
446
  );
171
447
 
172
- // Memoize QR box style
173
448
  const qrBoxStyle = useMemo(
174
449
  () => [
175
450
  styles.qrBox,
@@ -181,24 +456,44 @@ export const QrLogin: React.FC<QrLoginProps> = React.memo(
181
456
  [colors.primary, colors.skeletonBaseColor]
182
457
  );
183
458
 
184
- // Memoize scan button style
185
459
  const scanBtnStyle = useMemo(
186
460
  () => [styles.scanBtn, { borderColor: colors.button }],
187
461
  [colors.button]
188
462
  );
189
463
 
190
- // Memoize scan text style
191
464
  const scanTextStyle = useMemo(
192
- () => [styles.scanText, { color: colors.button }],
465
+ () => createTextStyle(styles.scanText, colors.button),
193
466
  [colors.button]
194
467
  );
195
468
 
196
- // Memoize no camera text style
197
469
  const noCameraTextStyle = useMemo(
198
470
  () => ({ color: colors.textPrimary, fontSize: RFValue(16) }),
199
471
  [colors.textPrimary]
200
472
  );
201
473
 
474
+ const cameraWrapperStyle = useMemo(
475
+ () => [
476
+ styles.cameraWrapper,
477
+ {
478
+ top: statusBarHeight,
479
+ },
480
+ ],
481
+ [statusBarHeight]
482
+ );
483
+
484
+ const componentStyles = useMemo(
485
+ () => ({
486
+ descText: descTextStyle,
487
+ qrBox: qrBoxStyle,
488
+ scanBtn: scanBtnStyle,
489
+ scanText: scanTextStyle,
490
+ iconMargin: styles.iconMargin,
491
+ buttonIconMargin: styles.buttonIconMargin,
492
+ loaderMargin: styles.loaderMargin,
493
+ }),
494
+ [descTextStyle, qrBoxStyle, scanBtnStyle, scanTextStyle]
495
+ );
496
+
202
497
  return (
203
498
  <View style={containerStyle}>
204
499
  <AppStatusBar theme={theme} />
@@ -206,72 +501,35 @@ export const QrLogin: React.FC<QrLoginProps> = React.memo(
206
501
  <View style={headerWrapperStyle}>
207
502
  <AppHeader
208
503
  title={title}
209
- onBackPress={onBackPress}
504
+ onBackPress={handleBackPress}
210
505
  theme={theme}
211
506
  titleAlign="left"
212
507
  />
213
508
  </View>
214
509
 
215
- <View style={styles.cameraWrapper}>
510
+ <View style={cameraWrapperStyle}>
216
511
  {scanActive ? (
217
512
  device ? (
218
- <>
219
- <Camera
220
- style={StyleSheet.absoluteFill}
221
- device={device}
222
- isActive={true}
223
- codeScanner={codeScanner}
224
- enableFpsGraph={false}
225
- />
226
- <QrViewArea theme={theme} headerHeight={HEADER_HEIGHT} />
227
- </>
513
+ <CameraView
514
+ device={device}
515
+ codeScanner={codeScanner}
516
+ theme={theme}
517
+ />
228
518
  ) : (
229
- <View style={centerStyle}>
230
- <Text style={noCameraTextStyle}>No camera available</Text>
231
- </View>
519
+ <NoCameraView
520
+ textStyle={noCameraTextStyle}
521
+ containerStyle={centerStyle}
522
+ />
232
523
  )
233
524
  ) : (
234
- <View style={styles.content}>
235
- <View style={styles.card}>
236
- <CameraIcon
237
- size={scale(50)}
238
- color={colors.button}
239
- strokeWidth={1.6}
240
- style={styles.iconMargin}
241
- />
242
-
243
- <Text style={descTextStyle}>{description}</Text>
244
-
245
- <View style={qrBoxStyle}>
246
- <QrCode
247
- size={scale(180)}
248
- color={colors.button}
249
- strokeWidth={1.6}
250
- />
251
- </View>
252
-
253
- <TouchableOpacity
254
- style={scanBtnStyle}
255
- onPress={startScan}
256
- activeOpacity={0.85}
257
- >
258
- <CameraIcon
259
- size={scale(22)}
260
- color={colors.button}
261
- strokeWidth={2}
262
- style={styles.buttonIconMargin}
263
- />
264
- <Text style={scanTextStyle}>{scanButtonText}</Text>
265
- </TouchableOpacity>
266
-
267
- {requesting && (
268
- <ActivityIndicator
269
- style={styles.loaderMargin}
270
- color={colors.primary}
271
- />
272
- )}
273
- </View>
274
- </View>
525
+ <ScanPreviewScreen
526
+ description={description}
527
+ scanButtonText={scanButtonText}
528
+ requesting={requesting}
529
+ onStartScan={startScan}
530
+ colors={colors}
531
+ styles={componentStyles}
532
+ />
275
533
  )}
276
534
  </View>
277
535
  </View>
@@ -279,6 +537,9 @@ export const QrLogin: React.FC<QrLoginProps> = React.memo(
279
537
  }
280
538
  );
281
539
 
540
+ QrLogin.displayName = 'QrLogin';
541
+
542
+ // ==================== Styles ====================
282
543
  const styles = StyleSheet.create({
283
544
  container: { flex: 1 },
284
545
  headerWrapper: {
@@ -291,7 +552,6 @@ const styles = StyleSheet.create({
291
552
  },
292
553
  cameraWrapper: {
293
554
  flex: 1,
294
- top: HEADER_HEIGHT,
295
555
  },
296
556
  center: {
297
557
  flex: 1,
@@ -306,8 +566,8 @@ const styles = StyleSheet.create({
306
566
  marginBottom: scale(50),
307
567
  },
308
568
  card: {
309
- width: SCREEN_W - scale(40),
310
- minHeight: SCREEN_H / 2.5,
569
+ width: SCREEN_W - scale(CARD_WIDTH_OFFSET),
570
+ minHeight: SCREEN_H / CARD_MIN_HEIGHT_RATIO,
311
571
  alignItems: 'center',
312
572
  padding: scale(20),
313
573
  },