react-native-expo-cropper 1.2.44 → 1.2.46

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.
@@ -1,352 +1,355 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import {
3
- StyleSheet,
4
- Text,
5
- View,
6
- TouchableOpacity,
7
- Alert,
8
- SafeAreaView,
9
- Dimensions,
10
- Image,
11
- Platform,
12
- } from 'react-native';
13
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
- import { Camera, CameraView } from 'expo-camera';
15
- const { width } = Dimensions.get('window');
16
-
17
- export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
18
- const [isReady, setIsReady] = useState(false);
19
- const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
20
- const [hasPermission, setHasPermission] = useState(null);
21
- const cameraRef = useRef(null);
22
- const cameraWrapperRef = useRef(null);
23
- const insets = useSafeAreaInsets();
24
- const [cameraWrapperLayout, setCameraWrapperLayout] = useState({ width: 0, height: 0, x: 0, y: 0 });
25
- const [greenFrame, setGreenFrame] = useState(null);
26
-
27
- useEffect(() => {
28
- (async () => {
29
- const { status } = await Camera.requestCameraPermissionsAsync();
30
- setHasPermission(status === 'granted');
31
- })();
32
- }, []);
33
-
34
- // Helper function to wait for multiple render cycles (works on iOS)
35
- const waitForRender = (cycles = 5) => {
36
- return new Promise((resolve) => {
37
- let count = 0;
38
- const tick = () => {
39
- count++;
40
- if (count >= cycles) {
41
- resolve();
42
- } else {
43
- setImmediate(tick);
44
- }
45
- };
46
- setImmediate(tick);
47
- });
48
- };
49
-
50
-
51
-
52
- // ✅ CRITICAL FIX: Calculate green frame coordinates relative to camera preview
53
- // The green frame should be calculated on the wrapper (as it's visually drawn there)
54
- // But we store it with wrapper dimensions so ImageCropper can map it correctly
55
- //
56
- // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
57
- // This is handled in ImageCropper by using "cover" mode to match preview content
58
- const calculateGreenFrameCoordinates = () => {
59
- const wrapperWidth = cameraWrapperLayout.width;
60
- const wrapperHeight = cameraWrapperLayout.height;
61
-
62
- if (wrapperWidth === 0 || wrapperHeight === 0) {
63
- console.warn("Camera wrapper layout not ready, cannot calculate green frame");
64
- return null;
65
- }
66
-
67
- // Calculate green frame as percentage of WRAPPER
68
- const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
69
- const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
70
- const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
71
- const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
72
-
73
- const frameCoords = {
74
- x: frameX,
75
- y: frameY,
76
- width: frameWidth,
77
- height: frameHeight,
78
- wrapperWidth,
79
- wrapperHeight,
80
- // ✅ Store percentages for easier mapping later
81
- percentX: (frameX / wrapperWidth) * 100,
82
- percentY: (frameY / wrapperHeight) * 100,
83
- percentWidth: 85, // 85% of wrapper width
84
- percentHeight: 70 // 70% of wrapper height
85
- };
86
-
87
- console.log("✅ Green frame coordinates calculated:", frameCoords);
88
- return frameCoords;
89
- };
90
-
91
- // 🔁 Keep green frame state in sync with wrapper layout so we can both render it
92
- // and send the exact same coordinates along with the captured photo.
93
- useEffect(() => {
94
- const coords = calculateGreenFrameCoordinates();
95
- if (coords) {
96
- setGreenFrame(coords);
97
- }
98
- }, [cameraWrapperLayout]);
99
-
100
- const takePicture = async () => {
101
- if (cameraRef.current) {
102
- try {
103
- // Show loading after a delay (using setImmediate for iOS compatibility)
104
- waitForRender(5).then(() => {
105
- setLoadingBeforeCapture(true);
106
- });
107
-
108
- // Wait a bit before taking picture (works on iOS)
109
- await waitForRender(2);
110
-
111
- // ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
112
- // Platform-specific optimizations for best quality
113
- const captureOptions = {
114
- // Maximum quality (0-1, 1 = best quality, no compression)
115
- quality: 1,
116
-
117
- // Disable shutter sound for better UX
118
- shutterSound: false,
119
-
120
- // ✅ CRITICAL: skipProcessing preserves original resolution and avoids interpolation
121
- // This ensures pixel-perfect quality and prevents premature resizing
122
- skipProcessing: true,
123
-
124
- // Include EXIF metadata (orientation, camera settings, etc.)
125
- exif: true,
126
-
127
- // ✅ Platform-specific optimizations
128
- ...(Platform.OS === 'ios' && {
129
- // iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
130
- // No additional processing to preserve quality
131
- }),
132
- ...(Platform.OS === 'android' && {
133
- // Android: Ensure maximum resolution capture
134
- // skipProcessing already handles this, but we can add Android-specific options if needed
135
- }),
136
- };
137
-
138
- console.log("📸 Capturing photo with maximum quality settings:", {
139
- platform: Platform.OS,
140
- options: captureOptions,
141
- wrapperSize: { width: cameraWrapperLayout.width, height: cameraWrapperLayout.height }
142
- });
143
-
144
- const photo = await cameraRef.current.takePictureAsync(captureOptions);
145
-
146
- // ✅ Validate captured photo dimensions
147
- if (!photo.width || !photo.height || photo.width === 0 || photo.height === 0) {
148
- throw new Error("Invalid photo dimensions received from camera");
149
- }
150
-
151
- const capturedAspectRatio = photo.width / photo.height;
152
- console.log(" Photo captured with maximum quality:", {
153
- uri: photo.uri,
154
- width: photo.width,
155
- height: photo.height,
156
- aspectRatio: capturedAspectRatio.toFixed(3),
157
- expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
158
- exif: photo.exif ? "present" : "missing",
159
- fileSize: photo.uri ? "available" : "unknown"
160
- });
161
-
162
- // ✅ CRITICAL FIX: Use the same green frame coordinates that are used for rendering
163
- // Fallback to recalculation if, for some reason, state is not yet set
164
- const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
165
-
166
- if (!greenFrameCoords) {
167
- throw new Error("Green frame coordinates not available");
168
- }
169
-
170
- // Send photo URI and frame data to ImageCropper
171
- // The photo maintains its native resolution and aspect ratio
172
- // ImageCropper will handle display and cropping while preserving quality
173
- onPhotoCaptured(photo.uri, {
174
- greenFrame: greenFrameCoords,
175
- capturedImageSize: {
176
- width: photo.width,
177
- height: photo.height,
178
- aspectRatio: capturedAspectRatio
179
- }
180
- });
181
-
182
- setLoadingBeforeCapture(false);
183
- } catch (error) {
184
- console.error("❌ Error capturing photo:", error);
185
- setLoadingBeforeCapture(false);
186
- Alert.alert(
187
- "Erreur",
188
- `Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
189
- );
190
- }
191
- }
192
- };
193
-
194
-
195
- return (
196
- <SafeAreaView style={styles.outerContainer}>
197
- <View
198
- style={styles.cameraWrapper}
199
- ref={cameraWrapperRef}
200
- onLayout={(e) => {
201
- const layout = e.nativeEvent.layout;
202
- setCameraWrapperLayout({
203
- width: layout.width,
204
- height: layout.height,
205
- x: layout.x,
206
- y: layout.y
207
- });
208
- console.log("Camera wrapper layout updated:", layout);
209
- }}
210
- >
211
- <CameraView
212
- style={styles.camera}
213
- facing="back"
214
- ref={cameraRef}
215
- onCameraReady={() => {
216
- setIsReady(true);
217
- console.log("✅ Camera ready - Maximum quality capture enabled");
218
- }}
219
- // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
220
- // The wrapper constrains the view, but capture uses full sensor resolution
221
- // This ensures preview matches what will be captured
222
- />
223
-
224
- {/* Loading overlay */}
225
- {loadingBeforeCapture && (
226
- <>
227
- <View style={styles.loadingOverlay}>
228
- <Image
229
- source={require('../src/assets/loadingCamera.gif')}
230
- style={styles.loadingGif}
231
- resizeMode="contain"
232
- />
233
- </View>
234
- <View style={styles.touchBlocker} pointerEvents="auto" />
235
- </>
236
- )}
237
-
238
- {/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
239
- {greenFrame && (
240
- <View
241
- style={[
242
- styles.scanFrame,
243
- {
244
- left: greenFrame.x,
245
- top: greenFrame.y,
246
- width: greenFrame.width,
247
- height: greenFrame.height,
248
- },
249
- ]}
250
- />
251
- )}
252
- </View>
253
-
254
- <View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
255
- <TouchableOpacity
256
- style={styles.button}
257
- onPress={takePicture}
258
- disabled={!isReady || loadingBeforeCapture}
259
- />
260
- </View>
261
- </SafeAreaView>
262
- );
263
- }
264
-
265
- const PRIMARY_GREEN = '#198754';
266
- const DEEP_BLACK = '#0B0B0B';
267
- const GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
268
-
269
- const styles = StyleSheet.create({
270
- outerContainer: {
271
- flex: 1,
272
- backgroundColor: DEEP_BLACK,
273
- justifyContent: 'center',
274
- alignItems: 'center',
275
- },
276
- cameraWrapper: {
277
- width: width,
278
- aspectRatio: 9 / 16,
279
- borderRadius: 30,
280
- overflow: 'hidden',
281
- alignItems: 'center',
282
- justifyContent: 'center',
283
- position: 'relative',
284
- },
285
- camera: {
286
- ...StyleSheet.absoluteFillObject,
287
- },
288
- scanFrame: {
289
- position: 'absolute',
290
- borderWidth: 4,
291
- borderColor: PRIMARY_GREEN,
292
- borderRadius: 5,
293
- backgroundColor: 'rgba(0, 0, 0, 0.1)',
294
- },
295
- loadingOverlay: {
296
- ...StyleSheet.absoluteFillObject,
297
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
298
- zIndex: 20,
299
- justifyContent: 'center',
300
- alignItems: 'center',
301
- },
302
- loadingGif: {
303
- width: 100,
304
- height: 100,
305
- },
306
- touchBlocker: {
307
- ...StyleSheet.absoluteFillObject,
308
- zIndex: 21,
309
- backgroundColor: 'transparent',
310
- },
311
- cancelIcon: {
312
- position: 'absolute',
313
- top: 20,
314
- left: 20,
315
- backgroundColor: PRIMARY_GREEN,
316
- borderRadius: 5,
317
- padding: 8,
318
- },
319
- buttonContainer: {
320
- position: 'absolute',
321
- bottom: 0,
322
- marginBottom: 20,
323
- flexDirection: 'row',
324
- justifyContent: 'center',
325
- },
326
- button: {
327
- width: 80,
328
- height: 80,
329
- borderRadius: 50,
330
- backgroundColor: GLOW_WHITE,
331
- borderWidth: 5,
332
- borderColor: PRIMARY_GREEN,
333
- alignItems: 'center',
334
- justifyContent: 'center',
335
- },
336
- text: {
337
- fontSize: 18,
338
- color: GLOW_WHITE,
339
- },
340
- container: {
341
- flex: 1,
342
- backgroundColor: DEEP_BLACK,
343
- justifyContent: 'center',
344
- alignItems: 'center',
345
- padding: 20,
346
- },
347
- iconText: {
348
- fontSize: 18,
349
- color: GLOW_WHITE,
350
- fontWeight: '600',
351
- },
352
- });
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import {
3
+ StyleSheet,
4
+ Text,
5
+ View,
6
+ TouchableOpacity,
7
+ Alert,
8
+ SafeAreaView,
9
+ Image,
10
+ Platform,
11
+ useWindowDimensions,
12
+ } from 'react-native';
13
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
14
+ import { Camera, CameraView } from 'expo-camera';
15
+
16
+ // Max width for camera preview on large screens (tablets) so it doesn't stretch full width
17
+ const CAMERA_PREVIEW_MAX_WIDTH = 500;
18
+
19
+ export default function CustomCamera({ onPhotoCaptured, onFrameCalculated }) {
20
+ const { width: windowWidth } = useWindowDimensions();
21
+ const cameraPreviewWidth = Math.min(windowWidth, CAMERA_PREVIEW_MAX_WIDTH);
22
+ const [isReady, setIsReady] = useState(false);
23
+ const [loadingBeforeCapture, setLoadingBeforeCapture] = useState(false);
24
+ const [hasPermission, setHasPermission] = useState(null);
25
+ const cameraRef = useRef(null);
26
+ const cameraWrapperRef = useRef(null);
27
+ const insets = useSafeAreaInsets();
28
+ const [cameraWrapperLayout, setCameraWrapperLayout] = useState({ width: 0, height: 0, x: 0, y: 0 });
29
+ const [greenFrame, setGreenFrame] = useState(null);
30
+
31
+ useEffect(() => {
32
+ (async () => {
33
+ const { status } = await Camera.requestCameraPermissionsAsync();
34
+ setHasPermission(status === 'granted');
35
+ })();
36
+ }, []);
37
+
38
+ // Helper function to wait for multiple render cycles (works on iOS)
39
+ const waitForRender = (cycles = 5) => {
40
+ return new Promise((resolve) => {
41
+ let count = 0;
42
+ const tick = () => {
43
+ count++;
44
+ if (count >= cycles) {
45
+ resolve();
46
+ } else {
47
+ setImmediate(tick);
48
+ }
49
+ };
50
+ setImmediate(tick);
51
+ });
52
+ };
53
+
54
+
55
+
56
+ // CRITICAL FIX: Calculate green frame coordinates relative to camera preview
57
+ // The green frame should be calculated on the wrapper (as it's visually drawn there)
58
+ // But we store it with wrapper dimensions so ImageCropper can map it correctly
59
+ //
60
+ // NOTE: Camera capture aspect ratio (typically 4:3) may differ from wrapper aspect ratio (9:16)
61
+ // This is handled in ImageCropper by using "cover" mode to match preview content
62
+ const calculateGreenFrameCoordinates = () => {
63
+ const wrapperWidth = cameraWrapperLayout.width;
64
+ const wrapperHeight = cameraWrapperLayout.height;
65
+
66
+ if (wrapperWidth === 0 || wrapperHeight === 0) {
67
+ console.warn("Camera wrapper layout not ready, cannot calculate green frame");
68
+ return null;
69
+ }
70
+
71
+ // Calculate green frame as percentage of WRAPPER
72
+ const frameWidth = wrapperWidth * 0.85; // 85% of wrapper width
73
+ const frameHeight = wrapperHeight * 0.70; // 70% of wrapper height
74
+ const frameX = (wrapperWidth - frameWidth) / 2; // Centered horizontally
75
+ const frameY = (wrapperHeight - frameHeight) / 2; // Centered vertically
76
+
77
+ const frameCoords = {
78
+ x: frameX,
79
+ y: frameY,
80
+ width: frameWidth,
81
+ height: frameHeight,
82
+ wrapperWidth,
83
+ wrapperHeight,
84
+ // Store percentages for easier mapping later
85
+ percentX: (frameX / wrapperWidth) * 100,
86
+ percentY: (frameY / wrapperHeight) * 100,
87
+ percentWidth: 85, // 85% of wrapper width
88
+ percentHeight: 70 // 70% of wrapper height
89
+ };
90
+
91
+ console.log("✅ Green frame coordinates calculated:", frameCoords);
92
+ return frameCoords;
93
+ };
94
+
95
+ // 🔁 Keep green frame state in sync with wrapper layout so we can both render it
96
+ // and send the exact same coordinates along with the captured photo.
97
+ useEffect(() => {
98
+ const coords = calculateGreenFrameCoordinates();
99
+ if (coords) {
100
+ setGreenFrame(coords);
101
+ }
102
+ }, [cameraWrapperLayout]);
103
+
104
+ const takePicture = async () => {
105
+ if (cameraRef.current) {
106
+ try {
107
+ // Show loading after a delay (using setImmediate for iOS compatibility)
108
+ waitForRender(5).then(() => {
109
+ setLoadingBeforeCapture(true);
110
+ });
111
+
112
+ // Wait a bit before taking picture (works on iOS)
113
+ await waitForRender(2);
114
+
115
+ // ✅ OPTIMIZED: Capture with maximum quality and native camera ratio
116
+ // Platform-specific optimizations for best quality
117
+ const captureOptions = {
118
+ // Maximum quality (0-1, 1 = best quality, no compression)
119
+ quality: 1,
120
+
121
+ // Disable shutter sound for better UX
122
+ shutterSound: false,
123
+
124
+ // CRITICAL: skipProcessing preserves original resolution and avoids interpolation
125
+ // This ensures pixel-perfect quality and prevents premature resizing
126
+ skipProcessing: true,
127
+
128
+ // Include EXIF metadata (orientation, camera settings, etc.)
129
+ exif: true,
130
+
131
+ // ✅ Platform-specific optimizations
132
+ ...(Platform.OS === 'ios' && {
133
+ // iOS: Use native capture format (typically 4:3 or 16:9 depending on device)
134
+ // No additional processing to preserve quality
135
+ }),
136
+ ...(Platform.OS === 'android' && {
137
+ // Android: Ensure maximum resolution capture
138
+ // skipProcessing already handles this, but we can add Android-specific options if needed
139
+ }),
140
+ };
141
+
142
+ console.log("📸 Capturing photo with maximum quality settings:", {
143
+ platform: Platform.OS,
144
+ options: captureOptions,
145
+ wrapperSize: { width: cameraWrapperLayout.width, height: cameraWrapperLayout.height }
146
+ });
147
+
148
+ const photo = await cameraRef.current.takePictureAsync(captureOptions);
149
+
150
+ // ✅ Validate captured photo dimensions
151
+ if (!photo.width || !photo.height || photo.width === 0 || photo.height === 0) {
152
+ throw new Error("Invalid photo dimensions received from camera");
153
+ }
154
+
155
+ const capturedAspectRatio = photo.width / photo.height;
156
+ console.log("✅ Photo captured with maximum quality:", {
157
+ uri: photo.uri,
158
+ width: photo.width,
159
+ height: photo.height,
160
+ aspectRatio: capturedAspectRatio.toFixed(3),
161
+ expectedRatio: "~1.33 (4:3) or ~1.78 (16:9)",
162
+ exif: photo.exif ? "present" : "missing",
163
+ fileSize: photo.uri ? "available" : "unknown"
164
+ });
165
+
166
+ // CRITICAL FIX: Use the same green frame coordinates that are used for rendering
167
+ // Fallback to recalculation if, for some reason, state is not yet set
168
+ const greenFrameCoords = greenFrame || calculateGreenFrameCoordinates();
169
+
170
+ if (!greenFrameCoords) {
171
+ throw new Error("Green frame coordinates not available");
172
+ }
173
+
174
+ // ✅ Send photo URI and frame data to ImageCropper
175
+ // The photo maintains its native resolution and aspect ratio
176
+ // ImageCropper will handle display and cropping while preserving quality
177
+ onPhotoCaptured(photo.uri, {
178
+ greenFrame: greenFrameCoords,
179
+ capturedImageSize: {
180
+ width: photo.width,
181
+ height: photo.height,
182
+ aspectRatio: capturedAspectRatio
183
+ }
184
+ });
185
+
186
+ setLoadingBeforeCapture(false);
187
+ } catch (error) {
188
+ console.error("❌ Error capturing photo:", error);
189
+ setLoadingBeforeCapture(false);
190
+ Alert.alert(
191
+ "Erreur",
192
+ `Impossible de capturer la photo: ${error.message || "Erreur inconnue"}. Veuillez réessayer.`
193
+ );
194
+ }
195
+ }
196
+ };
197
+
198
+
199
+ return (
200
+ <SafeAreaView style={styles.outerContainer}>
201
+ <View
202
+ style={[styles.cameraWrapper, { width: cameraPreviewWidth }]}
203
+ ref={cameraWrapperRef}
204
+ onLayout={(e) => {
205
+ const layout = e.nativeEvent.layout;
206
+ setCameraWrapperLayout({
207
+ width: layout.width,
208
+ height: layout.height,
209
+ x: layout.x,
210
+ y: layout.y
211
+ });
212
+ console.log("Camera wrapper layout updated:", layout);
213
+ }}
214
+ >
215
+ <CameraView
216
+ style={styles.camera}
217
+ facing="back"
218
+ ref={cameraRef}
219
+ onCameraReady={() => {
220
+ setIsReady(true);
221
+ console.log("✅ Camera ready - Maximum quality capture enabled");
222
+ }}
223
+ // ✅ Ensure camera uses native aspect ratio (typically 4:3 for most devices)
224
+ // The wrapper constrains the view, but capture uses full sensor resolution
225
+ // This ensures preview matches what will be captured
226
+ />
227
+
228
+ {/* Loading overlay */}
229
+ {loadingBeforeCapture && (
230
+ <>
231
+ <View style={styles.loadingOverlay}>
232
+ <Image
233
+ source={require('../src/assets/loadingCamera.gif')}
234
+ style={styles.loadingGif}
235
+ resizeMode="contain"
236
+ />
237
+ </View>
238
+ <View style={styles.touchBlocker} pointerEvents="auto" />
239
+ </>
240
+ )}
241
+
242
+ {/* Cadre de scan - rendered using calculated coordinates (x, y, width, height) */}
243
+ {greenFrame && (
244
+ <View
245
+ style={[
246
+ styles.scanFrame,
247
+ {
248
+ left: greenFrame.x,
249
+ top: greenFrame.y,
250
+ width: greenFrame.width,
251
+ height: greenFrame.height,
252
+ },
253
+ ]}
254
+ />
255
+ )}
256
+ </View>
257
+
258
+ <View style={[styles.buttonContainer, { bottom: (insets?.bottom || 0) + 16, marginBottom: 0 }]}>
259
+ <TouchableOpacity
260
+ style={styles.button}
261
+ onPress={takePicture}
262
+ disabled={!isReady || loadingBeforeCapture}
263
+ />
264
+ </View>
265
+ </SafeAreaView>
266
+ );
267
+ }
268
+
269
+ const PRIMARY_GREEN = '#198754';
270
+ const DEEP_BLACK = '#0B0B0B';
271
+ const GLOW_WHITE = 'rgba(255, 255, 255, 0.85)';
272
+
273
+ const styles = StyleSheet.create({
274
+ outerContainer: {
275
+ flex: 1,
276
+ backgroundColor: DEEP_BLACK,
277
+ justifyContent: 'center',
278
+ alignItems: 'center',
279
+ },
280
+ cameraWrapper: {
281
+ aspectRatio: 9 / 16,
282
+ borderRadius: 30,
283
+ overflow: 'hidden',
284
+ alignItems: 'center',
285
+ justifyContent: 'center',
286
+ position: 'relative',
287
+ },
288
+ camera: {
289
+ ...StyleSheet.absoluteFillObject,
290
+ },
291
+ scanFrame: {
292
+ position: 'absolute',
293
+ borderWidth: 4,
294
+ borderColor: PRIMARY_GREEN,
295
+ borderRadius: 5,
296
+ backgroundColor: 'rgba(0, 0, 0, 0.1)',
297
+ },
298
+ loadingOverlay: {
299
+ ...StyleSheet.absoluteFillObject,
300
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
301
+ zIndex: 20,
302
+ justifyContent: 'center',
303
+ alignItems: 'center',
304
+ },
305
+ loadingGif: {
306
+ width: 100,
307
+ height: 100,
308
+ },
309
+ touchBlocker: {
310
+ ...StyleSheet.absoluteFillObject,
311
+ zIndex: 21,
312
+ backgroundColor: 'transparent',
313
+ },
314
+ cancelIcon: {
315
+ position: 'absolute',
316
+ top: 20,
317
+ left: 20,
318
+ backgroundColor: PRIMARY_GREEN,
319
+ borderRadius: 5,
320
+ padding: 8,
321
+ },
322
+ buttonContainer: {
323
+ position: 'absolute',
324
+ bottom: 0,
325
+ marginBottom: 20,
326
+ flexDirection: 'row',
327
+ justifyContent: 'center',
328
+ },
329
+ button: {
330
+ width: 80,
331
+ height: 80,
332
+ borderRadius: 50,
333
+ backgroundColor: GLOW_WHITE,
334
+ borderWidth: 5,
335
+ borderColor: PRIMARY_GREEN,
336
+ alignItems: 'center',
337
+ justifyContent: 'center',
338
+ },
339
+ text: {
340
+ fontSize: 18,
341
+ color: GLOW_WHITE,
342
+ },
343
+ container: {
344
+ flex: 1,
345
+ backgroundColor: DEEP_BLACK,
346
+ justifyContent: 'center',
347
+ alignItems: 'center',
348
+ padding: 20,
349
+ },
350
+ iconText: {
351
+ fontSize: 18,
352
+ color: GLOW_WHITE,
353
+ fontWeight: '600',
354
+ },
355
+ });