@sanctum-key/react-native-sdk 1.0.16 → 1.0.18
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/build/package.json +1 -1
- package/build/src/components/KYCElements/IDCardCapture.d.ts.map +1 -1
- package/build/src/components/KYCElements/IDCardCapture.js +113 -120
- package/build/src/components/KYCElements/IDCardCapture.js.map +1 -1
- package/build/src/modules/api/CardAuthentification.d.ts.map +1 -1
- package/build/src/modules/api/CardAuthentification.js +13 -63
- package/build/src/modules/api/CardAuthentification.js.map +1 -1
- package/build/src/utils/cropByObb.d.ts +0 -7
- package/build/src/utils/cropByObb.d.ts.map +1 -1
- package/build/src/utils/cropByObb.js +0 -8
- package/build/src/utils/cropByObb.js.map +1 -1
- package/package.json +1 -1
- package/src/components/KYCElements/IDCardCapture.tsx +566 -574
- package/src/modules/api/CardAuthentification.ts +21 -89
- package/src/utils/cropByObb.ts +1 -8
package/build/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAI5D,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;AAc5L,UAAU,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAAE;AAC3F,UAAU,kBAAkB;IAAG,SAAS,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAAE;AAExO,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"IDCardCapture.d.ts","sourceRoot":"","sources":["../../../../src/components/KYCElements/IDCardCapture.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAC;AAI5D,OAAO,EAAE,iBAAiB,EAAoI,MAAM,uBAAuB,CAAC;AAc5L,UAAU,cAAc;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAAE;AAC3F,UAAU,kBAAkB;IAAG,SAAS,EAAE,iBAAiB,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC,KAAK,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAAE;AAExO,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmftD,CAAC"}
|
|
@@ -157,8 +157,6 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
157
157
|
setProcessingImagePath(capturePath);
|
|
158
158
|
try {
|
|
159
159
|
let imagePathForUpload = capturePath;
|
|
160
|
-
// 🚨 THE FIX: Ignore the backend's broken absolute pixels.
|
|
161
|
-
// Calculate the crop using the Green UI Frame percentages (0.0 to 1.0)
|
|
162
160
|
const overlayBbox = cameraConfig.overlay.bbox;
|
|
163
161
|
const uiCropBbox = {
|
|
164
162
|
minX: overlayBbox.xMin / 100,
|
|
@@ -168,12 +166,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
168
166
|
};
|
|
169
167
|
try {
|
|
170
168
|
// Apply the crop using the UI percentages and a tight 2% tolerance
|
|
169
|
+
// NOTE: If you integrated the Kotlin centered crop function earlier, replace this with cropToCenterScanArea
|
|
171
170
|
imagePathForUpload = await cropImageWithBBoxWithTolerance(capturePath, uiCropBbox, 0.02);
|
|
172
|
-
// imagePathForUpload = await cropToCenterScanArea(capturePath, 0.91, 1.59);
|
|
173
171
|
}
|
|
174
172
|
catch (e) {
|
|
175
173
|
console.warn("Crop failed, falling back to original image", e);
|
|
176
|
-
imagePathForUpload = capturePath;
|
|
174
|
+
imagePathForUpload = capturePath;
|
|
177
175
|
}
|
|
178
176
|
const base64 = await pathToBase64(imagePathForUpload);
|
|
179
177
|
const newImages = {
|
|
@@ -261,8 +259,7 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
261
259
|
await autoCapture(result.path, verifiedResult);
|
|
262
260
|
}
|
|
263
261
|
catch (error) {
|
|
264
|
-
|
|
265
|
-
console.error("Backend Verification Error:", error);
|
|
262
|
+
console.log("Backend Verification Error:", error);
|
|
266
263
|
// 2. Define a map of technical keywords to user-friendly messages
|
|
267
264
|
const rawMessage = (error?.message || '').toLowerCase();
|
|
268
265
|
let userFriendlyMessage = 'Verification failed. Please try again.';
|
|
@@ -272,7 +269,8 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
272
269
|
else if (rawMessage.includes('country mismatch') || rawMessage.includes('pays sélectionné')) {
|
|
273
270
|
userFriendlyMessage = 'The document does not match your selected country.';
|
|
274
271
|
}
|
|
275
|
-
else if (rawMessage.includes('
|
|
272
|
+
else if (rawMessage.includes('card_not_fully_in_frame')) {
|
|
273
|
+
// 🚨 REMOVED 'too far' CHECK FROM HERE
|
|
276
274
|
userFriendlyMessage = 'Move closer to the document and ensure all corners are visible.';
|
|
277
275
|
}
|
|
278
276
|
// 3. Set ONLY the friendly message to the UI state
|
|
@@ -295,48 +293,48 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
295
293
|
}, [showCamera]);
|
|
296
294
|
if (!countrySelectionData || !selectedDocumentType) {
|
|
297
295
|
return (<View style={styles.root}>
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
<View style={styles.previewContainer}>
|
|
297
|
+
<Text style={styles.title}>{getLocalizedText(component.labels)}</Text>
|
|
298
|
+
<Text style={styles.description}>
|
|
299
|
+
{state.currentLanguage === "en" ? "Please complete the country and document selection first." : "Veuillez d'abord compléter la sélection du pays et du document."}
|
|
300
|
+
</Text>
|
|
301
|
+
</View>
|
|
302
|
+
</View>);
|
|
305
303
|
}
|
|
306
304
|
// --- CAMERA RENDER ---
|
|
307
305
|
if (showCamera) {
|
|
308
306
|
const isBusy = isProcessingCapture;
|
|
309
307
|
if (isRebootingCamera) {
|
|
310
308
|
return (<View style={[styles.root, { justifyContent: 'center', alignItems: 'center', backgroundColor: '#000' }]}>
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
309
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
310
|
+
<Text style={{ color: 'white', marginTop: 20, fontSize: 16 }}>Initializing Camera...</Text>
|
|
311
|
+
</View>);
|
|
314
312
|
}
|
|
315
313
|
return (<View style={styles.root}>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
314
|
+
<View style={[styles.cameraWrapper, { flex: 1 }]}>
|
|
315
|
+
|
|
316
|
+
<View style={styles.headerContainer}>
|
|
317
|
+
<Text style={styles.headerTitle}>
|
|
318
|
+
{selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : ''}
|
|
319
|
+
</Text>
|
|
320
|
+
<View style={styles.stepBadge}>
|
|
321
|
+
<Text style={styles.stepText}>
|
|
322
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
323
|
+
</Text>
|
|
324
|
+
</View>
|
|
325
|
+
</View>
|
|
326
|
+
|
|
327
|
+
<View style={[styles.cameraFeedContainer, { flex: 1, backgroundColor: '#000' }]}>
|
|
328
|
+
|
|
329
|
+
{/* WEB ONLY: Flip Camera Top Button */}
|
|
330
|
+
{Platform.OS === 'web' && (<View style={styles.webTopControls}>
|
|
331
|
+
<TouchableOpacity style={styles.webFlipButton} onPress={toggleCameraLens}>
|
|
332
|
+
<Text style={styles.webFlipText}>🔄 Flip Lens</Text>
|
|
333
|
+
</TouchableOpacity>
|
|
334
|
+
</View>)}
|
|
337
335
|
|
|
338
|
-
|
|
339
|
-
|
|
336
|
+
<EnhancedCameraView key={`${currentSide}-${cameraKey}`} showCamera={true} isProcessing={isBusy} cameraType={cameraConfig.cameraType} style={StyleSheet.absoluteFillObject} onError={handleError} onSilentCapture={handleSilentCapture} silentCaptureResult={silentCaptureResult} overlayComponent={<>
|
|
337
|
+
<IdCardOverlay xMin={cameraConfig.overlay.bbox.xMin} yMin={cameraConfig.overlay.bbox.yMin} xMax={cameraConfig.overlay.bbox.xMax} yMax={cameraConfig.overlay.bbox.yMax} instructions={cameraConfig.overlay.guideText} cornerOpacity={cameraConfig.overlay.bbox.cornerRadius || 0} isSuccess={silentCaptureResult.success} language={state.currentLanguage} stepperProps={{
|
|
340
338
|
back: () => {
|
|
341
339
|
if (currentSide === 'back') {
|
|
342
340
|
setCurrentSide('front');
|
|
@@ -358,86 +356,83 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
358
356
|
selectedDocumentType: selectedDocumentType ? (locale === 'en' ? getDocumentTypeInfo(selectedDocumentType.type).name.en : getDocumentTypeInfo(selectedDocumentType.type).name.fr) : '',
|
|
359
357
|
step: state.currentComponentIndex + 1, totalSteps: state.template.components.length, side: currentSide,
|
|
360
358
|
}}/>
|
|
361
|
-
|
|
359
|
+
</>}/>
|
|
362
360
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
361
|
+
{!isBusy && silentCaptureResult.isAnalyzing && (<View style={styles.topAnalyzingPillContainer}>
|
|
362
|
+
<View style={styles.topAnalyzingPill}>
|
|
363
|
+
<ActivityIndicator size="small" color="white"/>
|
|
364
|
+
<Text style={styles.analyzingPillText}>
|
|
365
|
+
{state.currentLanguage === 'en' ? 'Scanning...' : 'Analyse...'}
|
|
366
|
+
</Text>
|
|
367
|
+
</View>
|
|
368
|
+
</View>)}
|
|
371
369
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
370
|
+
{isBusy && (<View style={[StyleSheet.absoluteFillObject, { zIndex: 9999 }]}>
|
|
371
|
+
{processingImagePath && (<Image source={{ uri: processingImagePath.startsWith('file://') ? processingImagePath : `file://${processingImagePath}` }} style={StyleSheet.absoluteFillObject} resizeMode="cover"/>)}
|
|
372
|
+
<View style={styles.processingOverlay}>
|
|
373
|
+
<ActivityIndicator size="large" color="#2DBD60"/>
|
|
374
|
+
<Text style={styles.processingText}>
|
|
375
|
+
{state.currentLanguage === 'en' ? 'Perfect!\nProcessing Document...' : 'Parfait!\nTraitement du document...'}
|
|
376
|
+
</Text>
|
|
377
|
+
</View>
|
|
378
|
+
</View>)}
|
|
381
379
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
380
|
+
{/* 🚨 THE ESCAPE HATCH: Branching UI for Web vs Mobile */}
|
|
381
|
+
{!isBusy && Platform.OS === 'web' ? (<View style={styles.webBottomControlBar}>
|
|
382
|
+
{silentCaptureResult.error ? (<View style={styles.floatingErrorBanner}>
|
|
383
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
384
|
+
</View>) : <View style={{ height: 10 }}/>}
|
|
387
385
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
386
|
+
<View style={styles.webActionButtonsRow}>
|
|
387
|
+
<TouchableOpacity style={styles.webSecondaryButton} onPress={() => setShowCamera(false)}>
|
|
388
|
+
<Text style={styles.webSecondaryButtonText}>Cancel</Text>
|
|
389
|
+
</TouchableOpacity>
|
|
390
|
+
<TouchableOpacity style={styles.webPrimaryButton} onPress={refreshCamera}>
|
|
391
|
+
<Text style={styles.webPrimaryButtonText}>↻ Refresh Camera</Text>
|
|
392
|
+
</TouchableOpacity>
|
|
393
|
+
</View>
|
|
394
|
+
</View>) : !isBusy ? (<View style={styles.escapeHatchContainer}>
|
|
395
|
+
<TouchableOpacity style={styles.fallbackRefreshButton} onPress={refreshCamera}>
|
|
396
|
+
<Text style={styles.fallbackRefreshText}>↻ Refresh Camera</Text>
|
|
397
|
+
</TouchableOpacity>
|
|
398
|
+
<TouchableOpacity style={[styles.fallbackRefreshButton, { marginTop: 15, backgroundColor: 'rgba(220, 38, 38, 0.8)', borderColor: '#DC2626' }]} onPress={() => setShowCamera(false)}>
|
|
399
|
+
<Text style={styles.fallbackRefreshText}>Cancel / Go Back</Text>
|
|
400
|
+
</TouchableOpacity>
|
|
401
|
+
</View>) : null}
|
|
404
402
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
403
|
+
{silentCaptureResult.error && !isBusy && Platform.OS !== 'web' ? (<View style={[styles.floatingErrorBanner, { zIndex: 10000 }]}>
|
|
404
|
+
<Text style={styles.floatingErrorText}>⚠️ {silentCaptureResult.error}</Text>
|
|
405
|
+
</View>) : null}
|
|
408
406
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
407
|
+
</View>
|
|
408
|
+
</View>
|
|
409
|
+
</View>);
|
|
412
410
|
}
|
|
413
411
|
// --- PREVIEW RENDER ---
|
|
414
412
|
return (<View style={styles.root}>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
<View style={styles.imagePreviewWrapper}>
|
|
434
|
-
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
435
|
-
</View>
|
|
413
|
+
<View style={styles.previewContainer}>
|
|
414
|
+
<ScrollView style={styles.previewItemContainer} showsVerticalScrollIndicator={false}>
|
|
415
|
+
<View key={currentSide} style={styles.sideContainer}>
|
|
416
|
+
<Text style={styles.sideTitle}>
|
|
417
|
+
{t('kyc.idCardCapture.captureTitle', { side: currentSide === 'front' ? locale === 'en' ? 'Front' : 'Recto' : locale === 'en' ? 'Back' : 'Verso' })}
|
|
418
|
+
</Text>
|
|
419
|
+
<Text style={{ fontSize: 14, color: '#666', textAlign: 'center', marginBottom: 16, lineHeight: 22 }}>
|
|
420
|
+
{getLocalizedText(component.instructions)}
|
|
421
|
+
</Text>
|
|
422
|
+
|
|
423
|
+
<View style={{ alignItems: 'center', justifyContent: 'center', flexDirection: "column", gap: 16 }}>
|
|
424
|
+
|
|
425
|
+
{/* 🚨 REMOVED THE ORANGE WARNING BANNER ENTIRELY */}
|
|
426
|
+
|
|
427
|
+
{/* 🚨 PREVIEW CONTAINER WITH PLATFORM OVERRIDES */}
|
|
428
|
+
<View style={styles.imagePreviewWrapper}>
|
|
429
|
+
{capturedImages[currentSide]?.dir ? (<Image source={{ uri: capturedImages[currentSide].dir }} style={styles.previewImage}/>) : silentCaptureResult.path ? (<Image source={{ uri: silentCaptureResult.path }} style={styles.previewImage}/>) : null}
|
|
430
|
+
</View>
|
|
436
431
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
432
|
+
{!capturedImages[currentSide]?.dir && (<Button title={state.currentLanguage === "en" ? "Start Scanning" : "Commencer la numérisation"} onPress={() => { setShowCamera(true); refreshCamera(); actions.showCustomStepper(false); }} variant="primary" size="large" fullWidth/>)}
|
|
433
|
+
{capturedImages[currentSide]?.dir && (<>
|
|
434
|
+
<Button title={t('kyc.idCardCapture.retakeButton')} onPress={() => retakePicture(currentSide)} variant="outline" size="medium" fullWidth/>
|
|
435
|
+
<Button title={t('common.next')} onPress={() => {
|
|
441
436
|
if (!selectedDocumentType) {
|
|
442
437
|
showAlert('Error', 'Document type not selected');
|
|
443
438
|
return;
|
|
@@ -454,12 +449,12 @@ export const IDCardCapture = ({ component, value = {}, onValueChange, error, lan
|
|
|
454
449
|
setProcessingImagePath(null);
|
|
455
450
|
}
|
|
456
451
|
}} variant="primary" size="large" fullWidth/>
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
452
|
+
</>)}
|
|
453
|
+
</View>
|
|
454
|
+
</View>
|
|
455
|
+
</ScrollView>
|
|
456
|
+
</View>
|
|
457
|
+
</View>);
|
|
463
458
|
};
|
|
464
459
|
const styles = StyleSheet.create({
|
|
465
460
|
root: {
|
|
@@ -511,8 +506,8 @@ const styles = StyleSheet.create({
|
|
|
511
506
|
imagePreviewWrapper: {
|
|
512
507
|
width: '100%', borderRadius: 12, padding: 1, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.18, shadowRadius: 8, elevation: 8, backgroundColor: '#f0f0f0',
|
|
513
508
|
...Platform.select({
|
|
514
|
-
web: { aspectRatio: 1.59, height: 'auto' },
|
|
515
|
-
default: { height: 220 }
|
|
509
|
+
web: { aspectRatio: 1.59, height: 'auto' },
|
|
510
|
+
default: { height: 220 }
|
|
516
511
|
})
|
|
517
512
|
},
|
|
518
513
|
previewImage: { width: '100%', height: '100%', borderRadius: 12, resizeMode: 'cover' },
|
|
@@ -525,8 +520,6 @@ const styles = StyleSheet.create({
|
|
|
525
520
|
floatingErrorText: { color: '#991B1B', fontSize: 14, fontWeight: '700', textAlign: 'center' },
|
|
526
521
|
processingOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.6)', justifyContent: 'center', alignItems: 'center', zIndex: 9999 },
|
|
527
522
|
processingText: { color: '#FFF', fontSize: 18, fontWeight: 'bold', marginTop: 16, textAlign: 'center' },
|
|
528
|
-
warningBanner: { backgroundColor: '#FF9500', padding: 12, borderRadius: 8, width: '100%', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, elevation: 4 },
|
|
529
|
-
warningText: { color: 'white', fontWeight: 'bold', textAlign: 'center', fontSize: 16 },
|
|
530
523
|
errorText: { color: '#dc2626', fontSize: 14, marginTop: 8, textAlign: 'center' },
|
|
531
524
|
topAnalyzingPillContainer: { position: 'absolute', top: Platform.OS === 'android' ? 60 : 50, left: 0, right: 0, alignItems: 'center', zIndex: 100 },
|
|
532
525
|
topAnalyzingPill: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.6)', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, gap: 8 },
|