@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.
- package/lib/module/components/Auth/QrLogin/QrLogin.js +304 -138
- package/lib/module/components/Auth/QrLogin/QrLogin.js.map +1 -1
- package/lib/module/components/Auth/QrLogin/components/QrViewArea.js +193 -141
- package/lib/module/components/Auth/QrLogin/components/QrViewArea.js.map +1 -1
- package/lib/module/components/Content/Card/Category/Category.js +83 -11
- package/lib/module/components/Content/Card/Category/Category.js.map +1 -1
- package/lib/module/components/Content/Card/NowWatching/NowWatching.js +237 -108
- package/lib/module/components/Content/Card/NowWatching/NowWatching.js.map +1 -1
- package/lib/module/components/Content/Card/Sliders/Styles/One.js +185 -126
- package/lib/module/components/Content/Card/Sliders/Styles/One.js.map +1 -1
- package/lib/module/components/Content/Card/Sliders/Styles/Two.js +139 -92
- package/lib/module/components/Content/Card/Sliders/Styles/Two.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Five.js +131 -48
- package/lib/module/components/Content/Card/Styles/Five.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Four.js +126 -59
- package/lib/module/components/Content/Card/Styles/Four.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/One.js +125 -50
- package/lib/module/components/Content/Card/Styles/One.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/RotateInOut.js +138 -53
- package/lib/module/components/Content/Card/Styles/RotateInOut.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Six.js +207 -115
- package/lib/module/components/Content/Card/Styles/Six.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Three.js +134 -79
- package/lib/module/components/Content/Card/Styles/Three.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/TopTen.js +186 -171
- package/lib/module/components/Content/Card/Styles/TopTen.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Two.js +144 -64
- package/lib/module/components/Content/Card/Styles/Two.js.map +1 -1
- package/lib/module/components/Content/Card/components/AdsPoster.js +162 -0
- package/lib/module/components/Content/Card/components/AdsPoster.js.map +1 -0
- package/lib/module/components/Content/Card/components/CardPoster.js +120 -136
- package/lib/module/components/Content/Card/components/CardPoster.js.map +1 -1
- package/lib/module/components/Content/Card/components/index.js +4 -0
- package/lib/module/components/Content/Card/components/index.js.map +1 -0
- package/lib/module/components/Content/Content.js +67 -27
- package/lib/module/components/Content/Content.js.map +1 -1
- package/lib/module/components/Content/Sections.js +32 -11
- package/lib/module/components/Content/Sections.js.map +1 -1
- package/lib/module/constants/dummySections.js +44 -4
- package/lib/module/constants/dummySections.js.map +1 -1
- package/lib/module/hooks/Images/index.js +5 -0
- package/lib/module/hooks/Images/index.js.map +1 -0
- package/lib/module/hooks/Images/useImageLoader.js +168 -0
- package/lib/module/hooks/Images/useImageLoader.js.map +1 -0
- package/lib/module/hooks/Images/useImageValidation.js +36 -0
- package/lib/module/hooks/Images/useImageValidation.js.map +1 -0
- package/lib/module/hooks/index.js +3 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useAdTracking.js +270 -0
- package/lib/module/hooks/useAdTracking.js.map +1 -0
- package/lib/module/hooks/useCards.js +164 -0
- package/lib/module/hooks/useCards.js.map +1 -0
- package/lib/module/hooks/usePaginatedSection.js +11 -6
- package/lib/module/hooks/usePaginatedSection.js.map +1 -1
- package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts +2 -0
- package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts.map +1 -1
- package/lib/typescript/src/components/Auth/QrLogin/components/QrViewArea.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Category/Category.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/NowWatching/NowWatching.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Sliders/Styles/One.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Sliders/Styles/Two.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/One.d.ts +15 -3
- package/lib/typescript/src/components/Content/Card/Styles/One.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts +1 -0
- package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts +13 -5
- package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts +1 -0
- package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts +26 -0
- package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts.map +1 -0
- package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts +3 -1
- package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/components/index.d.ts +2 -0
- package/lib/typescript/src/components/Content/Card/components/index.d.ts.map +1 -0
- package/lib/typescript/src/components/Content/Card/index.d.ts +76 -6
- package/lib/typescript/src/components/Content/Card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Content.d.ts +4 -3
- package/lib/typescript/src/components/Content/Content.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Sections.d.ts +20 -6
- package/lib/typescript/src/components/Content/Sections.d.ts.map +1 -1
- package/lib/typescript/src/constants/dummySections.d.ts +5 -0
- package/lib/typescript/src/constants/dummySections.d.ts.map +1 -1
- package/lib/typescript/src/hooks/Images/index.d.ts +3 -0
- package/lib/typescript/src/hooks/Images/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/Images/useImageLoader.d.ts +36 -0
- package/lib/typescript/src/hooks/Images/useImageLoader.d.ts.map +1 -0
- package/lib/typescript/src/hooks/Images/useImageValidation.d.ts +17 -0
- package/lib/typescript/src/hooks/Images/useImageValidation.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +3 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAdTracking.d.ts +39 -0
- package/lib/typescript/src/hooks/useAdTracking.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCards.d.ts +36 -0
- package/lib/typescript/src/hooks/useCards.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePaginatedSection.d.ts +12 -2
- package/lib/typescript/src/hooks/usePaginatedSection.d.ts.map +1 -1
- package/lib/typescript/src/types/sections/index.d.ts +7 -4
- package/lib/typescript/src/types/sections/index.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/components/Auth/QrLogin/QrLogin.tsx +382 -122
- package/src/components/Auth/QrLogin/components/QrViewArea.tsx +291 -197
- package/src/components/Content/Card/Category/Category.tsx +95 -8
- package/src/components/Content/Card/NowWatching/NowWatching.tsx +281 -136
- package/src/components/Content/Card/Sliders/Styles/One.tsx +244 -148
- package/src/components/Content/Card/Sliders/Styles/Two.tsx +171 -102
- package/src/components/Content/Card/Styles/Five.tsx +161 -62
- package/src/components/Content/Card/Styles/Four.tsx +164 -85
- package/src/components/Content/Card/Styles/One.tsx +161 -71
- package/src/components/Content/Card/Styles/RotateInOut.tsx +157 -60
- package/src/components/Content/Card/Styles/Six.tsx +242 -142
- package/src/components/Content/Card/Styles/Three.tsx +166 -133
- package/src/components/Content/Card/Styles/TopTen.tsx +230 -191
- package/src/components/Content/Card/Styles/Two.tsx +182 -79
- package/src/components/Content/Card/components/AdsPoster.tsx +202 -0
- package/src/components/Content/Card/components/CardPoster.tsx +134 -154
- package/src/components/Content/Card/components/index.ts +1 -0
- package/src/components/Content/Content.tsx +83 -45
- package/src/components/Content/Sections.tsx +51 -10
- package/src/constants/dummySections.ts +48 -1
- package/src/hooks/Images/index.ts +2 -0
- package/src/hooks/Images/useImageLoader.ts +206 -0
- package/src/hooks/Images/useImageValidation.ts +36 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAdTracking.ts +349 -0
- package/src/hooks/useCards.ts +228 -0
- package/src/hooks/usePaginatedSection.ts +26 -7
- 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, {
|
|
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:
|
|
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
|
-
|
|
72
|
-
const lastValueRef
|
|
73
|
-
|
|
318
|
+
// QR scan state management
|
|
319
|
+
const { scanTimer, resetTimer, lastValueRef, isScanning, resetScanState } =
|
|
320
|
+
useQrScanState();
|
|
74
321
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
96
|
-
if (lastValueRef.current === value) return;
|
|
352
|
+
// Prevent duplicate scans
|
|
353
|
+
if (!allowMultipleScans && lastValueRef.current === value) return;
|
|
97
354
|
|
|
98
|
-
// Clear
|
|
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
|
-
//
|
|
363
|
+
// Trigger success callback after delay
|
|
107
364
|
scanTimer.current = setTimeout(() => {
|
|
108
365
|
isScanning.current = true;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
[
|
|
388
|
+
[
|
|
389
|
+
onScanSuccess,
|
|
390
|
+
onScanError,
|
|
391
|
+
allowMultipleScans,
|
|
392
|
+
resetScanState,
|
|
393
|
+
isScanning,
|
|
394
|
+
lastValueRef,
|
|
395
|
+
resetTimer,
|
|
396
|
+
scanTimer,
|
|
397
|
+
]
|
|
124
398
|
);
|
|
125
399
|
|
|
126
|
-
//
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
}, [
|
|
419
|
+
}, [device, resetScanState, requestCameraPermission, onScanError]);
|
|
147
420
|
|
|
148
|
-
//
|
|
421
|
+
// Memoized styles
|
|
149
422
|
const containerStyle = useMemo(
|
|
150
|
-
() =>
|
|
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
|
-
() => [
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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={
|
|
504
|
+
onBackPress={handleBackPress}
|
|
210
505
|
theme={theme}
|
|
211
506
|
titleAlign="left"
|
|
212
507
|
/>
|
|
213
508
|
</View>
|
|
214
509
|
|
|
215
|
-
<View style={
|
|
510
|
+
<View style={cameraWrapperStyle}>
|
|
216
511
|
{scanActive ? (
|
|
217
512
|
device ? (
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
519
|
+
<NoCameraView
|
|
520
|
+
textStyle={noCameraTextStyle}
|
|
521
|
+
containerStyle={centerStyle}
|
|
522
|
+
/>
|
|
232
523
|
)
|
|
233
524
|
) : (
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
310
|
-
minHeight: SCREEN_H /
|
|
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
|
},
|