@switchlabs/verify-ai-react-native 2.4.2 → 2.4.4

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.
@@ -89,15 +89,19 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
89
89
  const cameraReadyRef = useRef(false);
90
90
  const cameraInitFailedRef = useRef(false);
91
91
  const permissionDeniedTrackedRef = useRef(false);
92
- // Track dimensions to detect orientation changes and remount camera
92
+ // Track dimensions for orientation detection and responsive layout
93
93
  const { width: windowWidth, height: windowHeight } = useWindowDimensions();
94
+ const isLandscape = windowWidth > windowHeight;
94
95
  const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
96
+ // Detect orientation changes and remount camera after rotation settles.
97
+ // On iOS, AVCaptureVideoPreviewLayer distorts if remounted during the rotation
98
+ // animation — the native preview layer initializes with transitional bounds.
99
+ // A short delay lets the layout stabilize before creating a fresh CameraView.
95
100
  useEffect(() => {
96
101
  const prev = prevDimensionsRef.current;
97
102
  const orientationChanged = (prev.width > prev.height) !== (windowWidth > windowHeight);
98
103
  prevDimensionsRef.current = { width: windowWidth, height: windowHeight };
99
104
  if (orientationChanged && !terminated) {
100
- // Force camera remount to fix preview distortion on iOS
101
105
  telemetry?.track('camera_orientation_remount', {
102
106
  component: 'scanner',
103
107
  metadata: {
@@ -107,7 +111,12 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
107
111
  });
108
112
  setCameraReady(false);
109
113
  cameraReadyRef.current = false;
110
- setCameraKey((k) => k + 1);
114
+ // Delay remount so iOS rotation animation completes before the new
115
+ // AVCaptureVideoPreviewLayer initializes with the final frame bounds.
116
+ const timer = setTimeout(() => {
117
+ setCameraKey((k) => k + 1);
118
+ }, 400);
119
+ return () => clearTimeout(timer);
111
120
  }
112
121
  }, [windowWidth, windowHeight, terminated]);
113
122
  // Resume camera when app returns from background/inactive (e.g. notification bar)
@@ -360,24 +369,31 @@ export function VerifyAIScanner({ onCapture, onResult, onError, overlay, style,
360
369
  return (_jsxs(View, { style: [styles.container, styles.permissionContainer, style], children: [_jsx(Text, { style: styles.permissionText, children: "Camera access is required for photo verification" }), _jsx(TouchableOpacity, { style: styles.permissionButton, onPress: requestPermission, children: _jsx(Text, { style: styles.permissionButtonText, children: "Grant Camera Access" }) })] }));
361
370
  }
362
371
  const showBottomCard = status === 'success' || status === 'error';
363
- return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: styles.topBar, children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsxs(View, { style: styles.guideContainer, children: [_jsxs(View, { style: [
372
+ return (_jsx(View, { style: [styles.container, style], children: _jsx(CameraView, { ref: cameraRef, style: styles.camera, facing: "back", enableTorch: !terminated && enableTorch, onCameraReady: onCameraReady, onMountError: onMountError, children: _jsxs(View, { style: styles.overlay, children: [overlay?.title && (_jsx(View, { style: [styles.topBar, isLandscape && styles.topBarLandscape], children: _jsx(Text, { style: styles.titleText, children: overlay.title }) })), overlay?.showGuideFrame && (_jsxs(View, { style: [styles.guideContainer, isLandscape && styles.guideContainerLandscape], children: [_jsxs(View, { style: [
364
373
  styles.guideFrame,
374
+ isLandscape && styles.guideFrameLandscape,
365
375
  overlay.guideFrameAspectRatio
366
376
  ? { aspectRatio: overlay.guideFrameAspectRatio }
367
377
  : undefined,
368
- ], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] })] }), overlay.guideCaption && (_jsx(Text, { style: styles.guideCaptionText, children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: styles.bottomArea, children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
378
+ ], children: [overlay.guideOverlayContent && (_jsx(View, { style: [StyleSheet.absoluteFill, { opacity: overlay.guideOverlayOpacity ?? 0.3 }], children: overlay.guideOverlayContent })), _jsx(View, { style: [styles.corner, styles.cornerTopLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerTopRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomLeft, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] }), _jsx(View, { style: [styles.corner, styles.cornerBottomRight, overlay.theme?.cornerColor ? { borderColor: overlay.theme.cornerColor } : undefined] })] }), overlay.guideCaption && (_jsx(Text, { style: styles.guideCaptionText, children: overlay.guideCaption }))] })), status === 'processing' && (_jsxs(View, { style: styles.processingOverlay, children: [_jsx(ActivityIndicator, { size: "large", color: "#fff" }), _jsx(Text, { style: styles.statusText, children: overlay?.processingMessage || 'Analyzing photo...' })] })), showBottomCard && _jsx(View, { style: styles.cardBackdrop }), _jsxs(View, { style: [styles.bottomArea, isLandscape && styles.bottomAreaLandscape], children: [status === 'success' && result && (_jsxs(View, { style: styles.resultCard, children: [_jsxs(View, { style: styles.resultCardHeader, children: [_jsx(View, { style: [
369
379
  styles.resultIconCircle,
370
- result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
371
- result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
372
- !result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
373
- ], children: _jsx(Text, { style: styles.resultIcon, children: result.is_compliant ? '\u2713' : '\u2717' }) }), _jsx(Text, { style: [
380
+ exhausted
381
+ ? styles.resultIconExhausted
382
+ : result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
383
+ !exhausted && result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
384
+ !exhausted && !result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
385
+ ], children: _jsx(Text, { style: styles.resultIcon, children: exhausted ? '\u2139' : result.is_compliant ? '\u2713' : '\u2717' }) }), _jsx(Text, { style: [
374
386
  styles.resultLabel,
375
- result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
376
- result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
377
- !result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
378
- ], children: result.is_compliant
379
- ? (overlay?.successMessage || 'Verified')
380
- : (overlay?.failureMessage || 'Not Verified') })] }), _jsx(Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && (() => {
387
+ exhausted
388
+ ? styles.resultLabelExhausted
389
+ : result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
390
+ !exhausted && result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
391
+ !exhausted && !result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
392
+ ], children: exhausted
393
+ ? (overlay?.exhaustedMessage || 'Submitted for review')
394
+ : result.is_compliant
395
+ ? (overlay?.successMessage || 'Verified')
396
+ : (overlay?.failureMessage || 'Not Verified') })] }), _jsx(Text, { style: styles.feedbackText, children: result.feedback })] })), status === 'error' && (() => {
381
397
  let errorTitle;
382
398
  let errorMessage;
383
399
  if (exhausted) {
@@ -430,6 +446,9 @@ const styles = StyleSheet.create({
430
446
  paddingHorizontal: 20,
431
447
  alignItems: 'center',
432
448
  },
449
+ topBarLandscape: {
450
+ paddingTop: 16,
451
+ },
433
452
  titleText: {
434
453
  color: '#fff',
435
454
  fontSize: 18,
@@ -442,6 +461,9 @@ const styles = StyleSheet.create({
442
461
  alignItems: 'center',
443
462
  paddingHorizontal: 40,
444
463
  },
464
+ guideContainerLandscape: {
465
+ paddingHorizontal: 80,
466
+ },
445
467
  guideCaptionText: {
446
468
  color: 'rgba(255,255,255,0.8)',
447
469
  fontSize: 13,
@@ -454,6 +476,11 @@ const styles = StyleSheet.create({
454
476
  width: '100%',
455
477
  aspectRatio: 4 / 3,
456
478
  },
479
+ guideFrameLandscape: {
480
+ width: undefined,
481
+ height: '100%',
482
+ aspectRatio: 4 / 3,
483
+ },
457
484
  corner: {
458
485
  position: 'absolute',
459
486
  width: CORNER_SIZE,
@@ -496,6 +523,9 @@ const styles = StyleSheet.create({
496
523
  paddingBottom: 40,
497
524
  alignItems: 'center',
498
525
  },
526
+ bottomAreaLandscape: {
527
+ paddingBottom: 16,
528
+ },
499
529
  instructionsText: {
500
530
  color: 'rgba(255, 255, 255, 0.8)',
501
531
  fontSize: 14,
@@ -572,6 +602,9 @@ const styles = StyleSheet.create({
572
602
  resultIconError: {
573
603
  backgroundColor: '#f59e0b',
574
604
  },
605
+ resultIconExhausted: {
606
+ backgroundColor: '#f59e0b',
607
+ },
575
608
  resultIcon: {
576
609
  color: '#fff',
577
610
  fontSize: 20,
@@ -590,6 +623,9 @@ const styles = StyleSheet.create({
590
623
  resultLabelError: {
591
624
  color: '#b45309',
592
625
  },
626
+ resultLabelExhausted: {
627
+ color: '#92400e',
628
+ },
593
629
  feedbackText: {
594
630
  color: '#4b5563',
595
631
  fontSize: 15,
package/lib/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "2.4.2";
1
+ export declare const SDK_VERSION = "2.4.4";
package/lib/version.js CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.2';
1
+ export const SDK_VERSION = '2.4.4';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchlabs/verify-ai-react-native",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
4
4
  "description": "React Native SDK for Verify AI - photo verification with AI vision processing",
5
5
  "main": "./lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -154,10 +154,15 @@ export function VerifyAIScanner({
154
154
  const cameraInitFailedRef = useRef(false);
155
155
  const permissionDeniedTrackedRef = useRef(false);
156
156
 
157
- // Track dimensions to detect orientation changes and remount camera
157
+ // Track dimensions for orientation detection and responsive layout
158
158
  const { width: windowWidth, height: windowHeight } = useWindowDimensions();
159
+ const isLandscape = windowWidth > windowHeight;
159
160
  const prevDimensionsRef = useRef({ width: windowWidth, height: windowHeight });
160
161
 
162
+ // Detect orientation changes and remount camera after rotation settles.
163
+ // On iOS, AVCaptureVideoPreviewLayer distorts if remounted during the rotation
164
+ // animation — the native preview layer initializes with transitional bounds.
165
+ // A short delay lets the layout stabilize before creating a fresh CameraView.
161
166
  useEffect(() => {
162
167
  const prev = prevDimensionsRef.current;
163
168
  const orientationChanged =
@@ -165,7 +170,6 @@ export function VerifyAIScanner({
165
170
  prevDimensionsRef.current = { width: windowWidth, height: windowHeight };
166
171
 
167
172
  if (orientationChanged && !terminated) {
168
- // Force camera remount to fix preview distortion on iOS
169
173
  telemetry?.track('camera_orientation_remount', {
170
174
  component: 'scanner',
171
175
  metadata: {
@@ -175,7 +179,13 @@ export function VerifyAIScanner({
175
179
  });
176
180
  setCameraReady(false);
177
181
  cameraReadyRef.current = false;
178
- setCameraKey((k) => k + 1);
182
+
183
+ // Delay remount so iOS rotation animation completes before the new
184
+ // AVCaptureVideoPreviewLayer initializes with the final frame bounds.
185
+ const timer = setTimeout(() => {
186
+ setCameraKey((k) => k + 1);
187
+ }, 400);
188
+ return () => clearTimeout(timer);
179
189
  }
180
190
  }, [windowWidth, windowHeight, terminated]);
181
191
 
@@ -480,16 +490,17 @@ export function VerifyAIScanner({
480
490
  {/* Overlay */}
481
491
  <View style={styles.overlay}>
482
492
  {overlay?.title && (
483
- <View style={styles.topBar}>
493
+ <View style={[styles.topBar, isLandscape && styles.topBarLandscape]}>
484
494
  <Text style={styles.titleText}>{overlay.title}</Text>
485
495
  </View>
486
496
  )}
487
497
 
488
498
  {overlay?.showGuideFrame && (
489
- <View style={styles.guideContainer}>
499
+ <View style={[styles.guideContainer, isLandscape && styles.guideContainerLandscape]}>
490
500
  <View
491
501
  style={[
492
502
  styles.guideFrame,
503
+ isLandscape && styles.guideFrameLandscape,
493
504
  overlay.guideFrameAspectRatio
494
505
  ? { aspectRatio: overlay.guideFrameAspectRatio }
495
506
  : undefined,
@@ -527,33 +538,39 @@ export function VerifyAIScanner({
527
538
  {showBottomCard && <View style={styles.cardBackdrop} />}
528
539
 
529
540
  {/* Bottom area: instructions + capture button OR result card */}
530
- <View style={styles.bottomArea}>
541
+ <View style={[styles.bottomArea, isLandscape && styles.bottomAreaLandscape]}>
531
542
  {status === 'success' && result && (
532
543
  <View style={styles.resultCard}>
533
544
  <View style={styles.resultCardHeader}>
534
545
  <View
535
546
  style={[
536
547
  styles.resultIconCircle,
537
- result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
538
- result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
539
- !result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
548
+ exhausted
549
+ ? styles.resultIconExhausted
550
+ : result.is_compliant ? styles.resultIconPass : styles.resultIconFail,
551
+ !exhausted && result.is_compliant && overlay?.theme?.successColor ? { backgroundColor: overlay.theme.successColor } : undefined,
552
+ !exhausted && !result.is_compliant && overlay?.theme?.failureColor ? { backgroundColor: overlay.theme.failureColor } : undefined,
540
553
  ]}
541
554
  >
542
555
  <Text style={styles.resultIcon}>
543
- {result.is_compliant ? '\u2713' : '\u2717'}
556
+ {exhausted ? '\u2139' : result.is_compliant ? '\u2713' : '\u2717'}
544
557
  </Text>
545
558
  </View>
546
559
  <Text
547
560
  style={[
548
561
  styles.resultLabel,
549
- result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
550
- result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
551
- !result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
562
+ exhausted
563
+ ? styles.resultLabelExhausted
564
+ : result.is_compliant ? styles.resultLabelPass : styles.resultLabelFail,
565
+ !exhausted && result.is_compliant && overlay?.theme?.successColor ? { color: overlay.theme.successColor } : undefined,
566
+ !exhausted && !result.is_compliant && overlay?.theme?.failureColor ? { color: overlay.theme.failureColor } : undefined,
552
567
  ]}
553
568
  >
554
- {result.is_compliant
555
- ? (overlay?.successMessage || 'Verified')
556
- : (overlay?.failureMessage || 'Not Verified')}
569
+ {exhausted
570
+ ? (overlay?.exhaustedMessage || 'Submitted for review')
571
+ : result.is_compliant
572
+ ? (overlay?.successMessage || 'Verified')
573
+ : (overlay?.failureMessage || 'Not Verified')}
557
574
  </Text>
558
575
  </View>
559
576
  <Text style={styles.feedbackText}>{result.feedback}</Text>
@@ -653,6 +670,9 @@ const styles = StyleSheet.create({
653
670
  paddingHorizontal: 20,
654
671
  alignItems: 'center',
655
672
  },
673
+ topBarLandscape: {
674
+ paddingTop: 16,
675
+ },
656
676
  titleText: {
657
677
  color: '#fff',
658
678
  fontSize: 18,
@@ -666,6 +686,9 @@ const styles = StyleSheet.create({
666
686
  alignItems: 'center',
667
687
  paddingHorizontal: 40,
668
688
  },
689
+ guideContainerLandscape: {
690
+ paddingHorizontal: 80,
691
+ },
669
692
  guideCaptionText: {
670
693
  color: 'rgba(255,255,255,0.8)',
671
694
  fontSize: 13,
@@ -678,6 +701,11 @@ const styles = StyleSheet.create({
678
701
  width: '100%',
679
702
  aspectRatio: 4 / 3,
680
703
  },
704
+ guideFrameLandscape: {
705
+ width: undefined,
706
+ height: '100%',
707
+ aspectRatio: 4 / 3,
708
+ },
681
709
  corner: {
682
710
  position: 'absolute',
683
711
  width: CORNER_SIZE,
@@ -721,6 +749,9 @@ const styles = StyleSheet.create({
721
749
  paddingBottom: 40,
722
750
  alignItems: 'center',
723
751
  },
752
+ bottomAreaLandscape: {
753
+ paddingBottom: 16,
754
+ },
724
755
  instructionsText: {
725
756
  color: 'rgba(255, 255, 255, 0.8)',
726
757
  fontSize: 14,
@@ -799,6 +830,9 @@ const styles = StyleSheet.create({
799
830
  resultIconError: {
800
831
  backgroundColor: '#f59e0b',
801
832
  },
833
+ resultIconExhausted: {
834
+ backgroundColor: '#f59e0b',
835
+ },
802
836
  resultIcon: {
803
837
  color: '#fff',
804
838
  fontSize: 20,
@@ -817,6 +851,9 @@ const styles = StyleSheet.create({
817
851
  resultLabelError: {
818
852
  color: '#b45309',
819
853
  },
854
+ resultLabelExhausted: {
855
+ color: '#92400e',
856
+ },
820
857
  feedbackText: {
821
858
  color: '#4b5563',
822
859
  fontSize: 15,
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SDK_VERSION = '2.4.2';
1
+ export const SDK_VERSION = '2.4.4';