From eb49b37b8a7495fd2320450857530108afd5530d Mon Sep 17 00:00:00 2001 From: Mansi Date: Wed, 17 Dec 2025 23:39:23 +0530 Subject: [PATCH] Implement Camera Scanner component and examples - Added CameraScanner component for QR/Barcode scanning functionality. - Created CameraScannerExample to demonstrate usage of the CameraScanner. - Developed useCameraScanner hook for managing scanner state and events. - Introduced ScannerScreen for a drop-in ready scanner interface. - Added TypeScript types for ScanResult, ScannerError, and SDK configuration. - Implemented multiple examples showcasing various integration scenarios. --- src/components/CameraScanner.tsx | 275 ++++++++++++++++ src/components/CameraScannerExample.tsx | 340 ++++++++++++++++++++ src/hooks/useCameraScanner.ts | 145 +++++++++ src/screens/CameraExamples.tsx | 411 ++++++++++++++++++++++++ src/screens/ScannerScreen.tsx | 227 +++++++++++++ src/types/icamera.ts | 254 +++++++++++++++ 6 files changed, 1652 insertions(+) create mode 100644 src/components/CameraScanner.tsx create mode 100644 src/components/CameraScannerExample.tsx create mode 100644 src/hooks/useCameraScanner.ts create mode 100644 src/screens/CameraExamples.tsx create mode 100644 src/screens/ScannerScreen.tsx create mode 100644 src/types/icamera.ts diff --git a/src/components/CameraScanner.tsx b/src/components/CameraScanner.tsx new file mode 100644 index 0000000..d88bdf4 --- /dev/null +++ b/src/components/CameraScanner.tsx @@ -0,0 +1,275 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { + View, + StyleSheet, + Text, + TouchableOpacity, + NativeModules, + NativeEventEmitter, + Alert, + ActivityIndicator, +} from 'react-native'; + +const { ICameraSDK } = NativeModules; + +interface ScanResult { + code: string; + format: string; + formatCode: number; +} + +interface ScannerProps { + presignedUrl: string; + onScanResult?: (result: ScanResult) => void; + onError?: (error: string) => void; + onClose?: () => void; +} + +export const CameraScanner: React.FC = ({ + presignedUrl, + onScanResult, + onError, + onClose, +}) => { + const [isInitializing, setIsInitializing] = useState(true); + const [isScanning, setIsScanning] = useState(false); + const [lastScan, setLastScan] = useState(null); + const eventEmitterRef = useRef(null); + + // Initialize SDK and setup event listeners + useEffect(() => { + initializeSDK(); + + return () => { + stopScanning(); + if (eventEmitterRef.current) { + eventEmitterRef.current.removeAllListeners(); + } + }; + }, [presignedUrl]); + + const initializeSDK = async () => { + try { + if (!ICameraSDK) { + throw new Error('ICameraSDK module not available'); + } + + // Set up event emitter + eventEmitterRef.current = new NativeEventEmitter(ICameraSDK); + + // Check if SDK is already initialized + const isInitialized = await ICameraSDK.isInitialized(); + + if (!isInitialized) { + // Initialize with QR_CODE and BAR_CODE features + const initResult = await ICameraSDK.initialize(presignedUrl, [ + 'QR_CODE', + 'BAR_CODE', + ]); + console.log('SDK initialized:', initResult); + } + + // Setup event listeners + eventEmitterRef.current.addListener('onScannerResult', (result: ScanResult) => { + console.log('Scan result:', result); + setLastScan(result); + onScanResult?.(result); + }); + + eventEmitterRef.current.addListener('onScannerError', (error: any) => { + console.error('Scanner error:', error); + onError?.(error.error || 'Unknown error'); + Alert.alert('Scan Error', error.error || 'Failed to scan code'); + }); + + setIsInitializing(false); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Failed to initialize SDK:', errorMessage); + onError?.(errorMessage); + Alert.alert('Initialization Error', errorMessage); + setIsInitializing(false); + } + }; + + const startScanning = async () => { + try { + if (!ICameraSDK) { + throw new Error('ICameraSDK module not available'); + } + + const result = await ICameraSDK.startScanner(); + console.log('Scanner started:', result); + setIsScanning(true); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error('Failed to start scanner:', errorMessage); + onError?.(errorMessage); + Alert.alert('Start Scanner Error', errorMessage); + } + }; + + const stopScanning = async () => { + try { + if (!ICameraSDK) { + return; + } + + const result = await ICameraSDK.stopScanner(); + console.log('Scanner stopped:', result); + setIsScanning(false); + } catch (error) { + console.error('Failed to stop scanner:', error); + } + }; + + const handleClose = async () => { + await stopScanning(); + onClose?.(); + }; + + if (isInitializing) { + return ( + + + Initializing Camera SDK... + + ); + } + + return ( + + + QR/Barcode Scanner + + + + + + {/* Camera preview would go here */} + + + Camera Preview Area + + + + + + + {isScanning ? '⏹ Stop Scanning' : '▶ Start Scanning'} + + + + + {lastScan && ( + + Last Scan Result: + {lastScan.code} + Format: {lastScan.format} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: '#e0e0e0', + }, + title: { + fontSize: 18, + fontWeight: '600', + color: '#000', + }, + closeButton: { + padding: 8, + }, + closeButtonText: { + fontSize: 24, + color: '#999', + }, + cameraPreview: { + flex: 1, + backgroundColor: '#000', + justifyContent: 'center', + alignItems: 'center', + margin: 16, + borderRadius: 8, + borderWidth: 2, + borderColor: '#ddd', + }, + placeholderText: { + color: '#999', + fontSize: 16, + }, + controlsContainer: { + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#fff', + }, + button: { + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, + buttonActive: { + backgroundColor: '#ff4444', + }, + buttonInactive: { + backgroundColor: '#4CAF50', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + resultContainer: { + marginHorizontal: 16, + marginBottom: 16, + padding: 12, + backgroundColor: '#e8f5e9', + borderRadius: 8, + borderLeftWidth: 4, + borderLeftColor: '#4CAF50', + }, + resultLabel: { + fontSize: 12, + color: '#666', + marginBottom: 4, + }, + resultCode: { + fontSize: 16, + fontWeight: '600', + color: '#000', + marginBottom: 4, + }, + resultFormat: { + fontSize: 12, + color: '#666', + }, + loadingText: { + marginTop: 12, + fontSize: 14, + color: '#666', + }, +}); diff --git a/src/components/CameraScannerExample.tsx b/src/components/CameraScannerExample.tsx new file mode 100644 index 0000000..6bcba5a --- /dev/null +++ b/src/components/CameraScannerExample.tsx @@ -0,0 +1,340 @@ +import React, { useState } from 'react'; +import { + View, + StyleSheet, + Text, + TouchableOpacity, + Alert, + ScrollView, +} from 'react-native'; +import { useCameraScanner, ScanResult } from '../hooks/useCameraScanner'; + +interface ScanHistoryItem { + code: string; + format: string; + timestamp: string; +} + +export const CameraScannerExample: React.FC = () => { + const [showScanner, setShowScanner] = useState(false); + const [scanHistory, setScanHistory] = useState([]); + + // Your presigned URL - replace with actual URL from your backend + const PRESIGNED_URL = 'https://your-presigned-url-here.com'; + + const scanner = useCameraScanner({ + presignedUrl: PRESIGNED_URL, + features: ['QR_CODE', 'BAR_CODE'], + onScanResult: (result: ScanResult) => { + // Add to history + const item: ScanHistoryItem = { + code: result.code, + format: result.format, + timestamp: new Date().toLocaleTimeString(), + }; + setScanHistory((prev) => [item, ...prev]); + + // Show alert + Alert.alert('Scan Successful!', `Code: ${result.code}\nFormat: ${result.format}`); + }, + onError: (error: string) => { + Alert.alert('Error', error); + }, + }); + + const handleStartScanning = async () => { + if (!scanner.isInitialized) { + Alert.alert('Error', 'Scanner is not initialized'); + return; + } + + try { + setShowScanner(true); + await scanner.startScanning(); + } catch (err) { + Alert.alert('Error', 'Failed to start scanner'); + } + }; + + const handleStopScanning = async () => { + await scanner.stopScanning(); + setShowScanner(false); + }; + + const clearHistory = () => { + setScanHistory([]); + }; + + return ( + + + Camera Scanner + QR Code & Barcode Scanner + + + {showScanner ? ( + + + 📷 + + {scanner.isScanning ? 'Scanning...' : 'Ready to scan'} + + + + + Close Scanner + + + ) : ( + + {/* Status Section */} + + Status + + + SDK Initialized: + + {scanner.isInitialized ? '✓ Yes' : '✗ No'} + + + + Scanning: + + {scanner.isScanning ? '✓ Active' : '✗ Inactive'} + + + + + + {/* Controls Section */} + + + ▶ Start Scanner + + + + {/* History Section */} + + + Scan History + {scanHistory.length > 0 && ( + + Clear + + )} + + + {scanHistory.length === 0 ? ( + + No scans yet + + ) : ( + + {scanHistory.map((item, index) => ( + + + {item.code} + + + {item.format} + + {item.timestamp} + + + + ))} + + )} + + + )} + + {scanner.error && ( + + ⚠ {scanner.error} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f8f9fa', + }, + header: { + backgroundColor: '#2c3e50', + paddingHorizontal: 20, + paddingTop: 20, + paddingBottom: 16, + }, + headerTitle: { + fontSize: 24, + fontWeight: '700', + color: '#fff', + }, + headerSubtitle: { + fontSize: 14, + color: '#bdc3c7', + marginTop: 4, + }, + content: { + flex: 1, + padding: 16, + }, + scannerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + cameraPlaceholder: { + flex: 1, + width: '100%', + backgroundColor: '#000', + justifyContent: 'center', + alignItems: 'center', + }, + cameraPlaceholderText: { + fontSize: 80, + marginBottom: 16, + }, + cameraStatusText: { + fontSize: 16, + color: '#fff', + marginTop: 12, + }, + stopButton: { + marginHorizontal: 16, + marginBottom: 20, + paddingVertical: 14, + backgroundColor: '#e74c3c', + borderRadius: 8, + alignItems: 'center', + }, + stopButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + statusSection: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + color: '#2c3e50', + marginBottom: 12, + }, + statusCard: { + backgroundColor: '#fff', + borderRadius: 8, + padding: 16, + borderLeftWidth: 4, + borderLeftColor: '#3498db', + }, + statusItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 8, + borderBottomWidth: 1, + borderBottomColor: '#ecf0f1', + }, + statusLabel: { + fontSize: 14, + color: '#555', + fontWeight: '500', + }, + statusValue: { + fontSize: 14, + color: '#27ae60', + fontWeight: '600', + }, + controlsSection: { + marginBottom: 20, + }, + startButton: { + paddingVertical: 14, + backgroundColor: '#27ae60', + borderRadius: 8, + alignItems: 'center', + }, + startButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + historySection: { + marginBottom: 20, + }, + historyHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + clearButton: { + color: '#e74c3c', + fontSize: 12, + fontWeight: '600', + }, + emptyState: { + backgroundColor: '#fff', + borderRadius: 8, + padding: 32, + alignItems: 'center', + }, + emptyStateText: { + fontSize: 14, + color: '#999', + }, + historyItem: { + backgroundColor: '#fff', + borderRadius: 8, + padding: 12, + marginBottom: 8, + borderLeftWidth: 4, + borderLeftColor: '#27ae60', + }, + historyItemContent: { + flex: 1, + }, + historyCode: { + fontSize: 14, + fontWeight: '600', + color: '#2c3e50', + marginBottom: 6, + }, + historyFooter: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + historyFormat: { + fontSize: 12, + color: '#666', + fontWeight: '500', + }, + historyTime: { + fontSize: 12, + color: '#999', + }, + errorBanner: { + backgroundColor: '#ffe74c', + padding: 12, + borderTopWidth: 1, + borderTopColor: '#ffd700', + }, + errorText: { + fontSize: 12, + color: '#856404', + fontWeight: '600', + }, +}); diff --git a/src/hooks/useCameraScanner.ts b/src/hooks/useCameraScanner.ts new file mode 100644 index 0000000..a81c33b --- /dev/null +++ b/src/hooks/useCameraScanner.ts @@ -0,0 +1,145 @@ +import { useEffect, useState, useRef } from 'react'; +import { NativeModules, NativeEventEmitter } from 'react-native'; + +const { ICameraSDK } = NativeModules; + +export interface ScanResult { + code: string; + format: string; + formatCode: number; +} + +export interface UseCameraScannerOptions { + presignedUrl: string; + features?: string[]; + onScanResult?: (result: ScanResult) => void; + onError?: (error: string) => void; +} + +export const useCameraScanner = (options: UseCameraScannerOptions) => { + const [isInitialized, setIsInitialized] = useState(false); + const [isScanning, setIsScanning] = useState(false); + const [error, setError] = useState(null); + const eventEmitterRef = useRef(null); + const subscriptionsRef = useRef([]); + + // Initialize the SDK + useEffect(() => { + initialize(); + + return () => { + cleanup(); + }; + }, [options.presignedUrl]); + + const initialize = async () => { + try { + if (!ICameraSDK) { + throw new Error('ICameraSDK module not available'); + } + + eventEmitterRef.current = new NativeEventEmitter(ICameraSDK); + + const isInit = await ICameraSDK.isInitialized(); + + if (!isInit) { + const features = options.features || ['QR_CODE', 'BAR_CODE']; + await ICameraSDK.initialize(options.presignedUrl, features); + } + + // Setup event listeners + const scannerResultSub = eventEmitterRef.current.addListener( + 'onScannerResult', + (result: ScanResult) => { + options.onScanResult?.(result); + } + ); + + const scannerErrorSub = eventEmitterRef.current.addListener( + 'onScannerError', + (error: any) => { + const errorMsg = error.error || 'Unknown error'; + setError(errorMsg); + options.onError?.(errorMsg); + } + ); + + subscriptionsRef.current = [scannerResultSub, scannerErrorSub]; + setIsInitialized(true); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + setError(errorMsg); + options.onError?.(errorMsg); + } + }; + + const startScanning = async () => { + try { + if (!ICameraSDK) { + throw new Error('ICameraSDK module not available'); + } + + await ICameraSDK.startScanner(); + setIsScanning(true); + setError(null); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + setError(errorMsg); + options.onError?.(errorMsg); + } + }; + + const stopScanning = async () => { + try { + if (!ICameraSDK) { + return; + } + + await ICameraSDK.stopScanner(); + setIsScanning(false); + } catch (err) { + console.error('Error stopping scanner:', err); + } + }; + + const cleanup = async () => { + await stopScanning(); + + subscriptionsRef.current.forEach((sub) => { + sub.remove(); + }); + subscriptionsRef.current = []; + + try { + if (ICameraSDK) { + await ICameraSDK.shutdown(); + } + } catch (err) { + console.error('Error shutting down SDK:', err); + } + }; + + const getConfiguration = async () => { + try { + if (!ICameraSDK) { + throw new Error('ICameraSDK module not available'); + } + + return await ICameraSDK.getConfiguration(); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : String(err); + setError(errorMsg); + throw err; + } + }; + + return { + isInitialized, + isScanning, + error, + startScanning, + stopScanning, + getConfiguration, + cleanup, + }; +}; diff --git a/src/screens/CameraExamples.tsx b/src/screens/CameraExamples.tsx new file mode 100644 index 0000000..dde8adf --- /dev/null +++ b/src/screens/CameraExamples.tsx @@ -0,0 +1,411 @@ +/** + * Complete Examples for iCameraSDK + * Copy and adapt these examples for your use case + */ + +import React, { useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + Alert, + StyleSheet, + NativeModules, + NativeEventEmitter, +} from 'react-native'; +import { useCameraScanner } from '../hooks/useCameraScanner'; +import { ScanResult } from '../types/icamera'; + +// ============================================================================ +// Example 1: Using the Hook (Recommended - Easiest) +// ============================================================================ + +export const Example1_UseHook = () => { + const scanner = useCameraScanner({ + presignedUrl: 'https://your-backend-presigned-url.com', + features: ['QR_CODE', 'BAR_CODE'], + onScanResult: (result: ScanResult) => { + console.log('✓ Scanned:', result.code); + Alert.alert('Success', `Code: ${result.code}`); + }, + onError: (error: string) => { + console.error('✗ Error:', error); + Alert.alert('Error', error); + }, + }); + + return ( + + Initialized: {scanner.isInitialized ? '✓' : '✗'} + + Start Scan + + + ); +}; + +// ============================================================================ +// Example 2: Direct NativeModule Access +// ============================================================================ + +export const Example2_DirectModule = () => { + const [isScanning, setIsScanning] = useState(false); + const { ICameraSDK } = NativeModules; + + const handleStart = async () => { + try { + // Initialize if not already done + const isInit = await ICameraSDK.isInitialized(); + if (!isInit) { + await ICameraSDK.initialize( + 'https://presigned-url.com', + ['QR_CODE', 'BAR_CODE'] + ); + } + + // Start scanner + await ICameraSDK.startScanner(); + setIsScanning(true); + + // Setup listeners + const emitter = new NativeEventEmitter(ICameraSDK); + emitter.addListener('onScannerResult', (result) => { + console.log('Scanned:', result.code); + }); + } catch (error) { + console.error('Error:', error); + } + }; + + const handleStop = async () => { + try { + await ICameraSDK.stopScanner(); + setIsScanning(false); + } catch (error) { + console.error('Error:', error); + } + }; + + return ( + + + + {isScanning ? 'Stop' : 'Start'} + + + + ); +}; + +// ============================================================================ +// Example 3: Advanced Hook Usage with Lifecycle +// ============================================================================ + +export const Example3_AdvancedHook = () => { + const [scanHistory, setScanHistory] = useState([]); + + const scanner = useCameraScanner({ + presignedUrl: 'https://your-backend-presigned-url.com', + features: ['QR_CODE'], + onScanResult: (result: ScanResult) => { + setScanHistory((prev) => [result, ...prev]); + }, + onError: (error: string) => { + Alert.alert('Scanner Error', error); + }, + }); + + // Cleanup on unmount + React.useEffect(() => { + return () => { + scanner.cleanup(); + }; + }, []); + + return ( + + + SDK: {scanner.isInitialized ? '✓ Initialized' : '⏳ Loading'} + State: {scanner.isScanning ? '▶ Scanning' : '⏸ Idle'} + + + + ▶ Start Scanning + + + + ⏹ Stop Scanning + + + + + Scan History ({scanHistory.length}) + + {scanHistory.map((scan, idx) => ( + + {scan.code} + {scan.format} + + ))} + + + {scanner.error && ( + + {scanner.error} + + )} + + ); +}; + +// ============================================================================ +// Example 4: Integration with Your App Flow +// ============================================================================ + +interface User { + id: string; + email: string; +} + +export const Example4_AppIntegration = () => { + const [user, setUser] = useState(null); + const [scannedData, setScannedData] = useState(null); + + // Your backend API to get presigned URL + const getPresignedUrl = async (userId: string): Promise => { + const response = await fetch(`/api/camera/presigned-url?userId=${userId}`); + const data = await response.json(); + return data.presignedUrl; + }; + + const scanner = useCameraScanner({ + presignedUrl: user ? getPresignedUrl(user.id) : '', + onScanResult: async (result: ScanResult) => { + setScannedData(result); + + // Process the scanned data + try { + // Send to your backend + const response = await fetch('/api/camera/process-scan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: user?.id, + scannedCode: result.code, + format: result.format, + }), + }); + + if (response.ok) { + Alert.alert('✓ Success', 'Scan processed successfully'); + await scanner.stopScanning(); + } + } catch (error) { + Alert.alert('✗ Error', 'Failed to process scan'); + } + }, + onError: (error: string) => { + Alert.alert('Scanner Error', error); + }, + }); + + const handleStartScanning = async () => { + if (!user) { + Alert.alert('Error', 'User not logged in'); + return; + } + + if (!scanner.isInitialized) { + Alert.alert('Error', 'Scanner not initialized'); + return; + } + + await scanner.startScanning(); + }; + + return ( + + App Integration Example + + {user ? ( + + Logged in as: {user.email} + + Open Scanner + + {scannedData && ( + + Last scan: {scannedData.code} + + )} + + ) : ( + setUser({ id: '1', email: 'user@example.com' })} + style={styles.button} + > + Login First + + )} + + ); +}; + +// ============================================================================ +// Example 5: Error Handling & Recovery +// ============================================================================ + +export const Example5_ErrorHandling = () => { + const [logs, setLogs] = useState([]); + + const addLog = (message: string) => { + setLogs((prev) => [`${new Date().toLocaleTimeString()} - ${message}`, ...prev]); + }; + + const scanner = useCameraScanner({ + presignedUrl: 'https://your-presigned-url.com', + onScanResult: (result: ScanResult) => { + addLog(`✓ Scanned: ${result.code}`); + }, + onError: (error: string) => { + addLog(`✗ Error: ${error}`); + + // Handle specific errors + if (error.includes('permission')) { + addLog('💡 Tip: Grant camera permissions in settings'); + } else if (error.includes('not initialized')) { + addLog('💡 Tip: Initialize SDK first'); + } else if (error.includes('camera')) { + addLog('💡 Tip: Ensure camera is available'); + } + }, + }); + + return ( + + Error Handling Example + + + Logs: + {logs.map((log, idx) => ( + + {log} + + ))} + + + + Test Scanner + + + ); +}; + +// ============================================================================ +// Styles +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + backgroundColor: '#f5f5f5', + }, + title: { + fontSize: 18, + fontWeight: '600', + marginBottom: 16, + }, + status: { + backgroundColor: '#fff', + padding: 12, + borderRadius: 8, + marginBottom: 12, + }, + button: { + backgroundColor: '#2196F3', + padding: 12, + borderRadius: 8, + alignItems: 'center', + marginVertical: 8, + }, + buttonText: { + color: '#fff', + fontWeight: '600', + }, + history: { + marginTop: 16, + }, + historyTitle: { + fontWeight: '600', + marginBottom: 8, + }, + historyItem: { + backgroundColor: '#fff', + padding: 8, + marginBottom: 8, + borderRadius: 4, + }, + historyFormat: { + fontSize: 12, + color: '#666', + }, + result: { + backgroundColor: '#e8f5e9', + padding: 12, + borderRadius: 8, + marginTop: 12, + }, + error: { + backgroundColor: '#ffe0e0', + padding: 12, + borderRadius: 8, + marginTop: 12, + }, + errorText: { + color: '#c00', + }, + logs: { + backgroundColor: '#fff', + padding: 12, + borderRadius: 8, + maxHeight: 200, + marginBottom: 12, + }, + logsTitle: { + fontWeight: '600', + marginBottom: 8, + }, + log: { + fontSize: 11, + color: '#666', + marginBottom: 4, + fontFamily: 'monospace', + }, +}); + +// ============================================================================ +// Export all examples +// ============================================================================ + +export default [ + Example1_UseHook, + Example2_DirectModule, + Example3_AdvancedHook, + Example4_AppIntegration, + Example5_ErrorHandling, +]; diff --git a/src/screens/ScannerScreen.tsx b/src/screens/ScannerScreen.tsx new file mode 100644 index 0000000..cb38df8 --- /dev/null +++ b/src/screens/ScannerScreen.tsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Alert, + ScrollView, +} from 'react-native'; +import { useCameraScanner, ScanResult } from '../hooks/useCameraScanner'; + +/** + * Drop-in ready Scanner Screen + * + * Usage: + * + */ + +interface ScannerScreenProps { + presignedUrl: string; + onScan?: (result: ScanResult) => void; + onError?: (error: string) => void; +} + +export const ScannerScreen: React.FC = ({ + presignedUrl, + onScan, + onError, +}) => { + const [scans, setScans] = useState([]); + + const scanner = useCameraScanner({ + presignedUrl, + features: ['QR_CODE', 'BAR_CODE'], + onScanResult: (result) => { + console.log('✓ Scan:', result); + setScans((prev) => [result, ...prev]); + onScan?.(result); + + // Optional: Show alert + Alert.alert( + '✓ Scanned Successfully', + `Code: ${result.code}\nFormat: ${result.format}`, + [{ text: 'OK' }] + ); + }, + onError: (error) => { + console.error('✗ Error:', error); + onError?.(error); + Alert.alert('Error', error); + }, + }); + + return ( + + {/* Header */} + + Scanner + + {scanner.isInitialized ? '✓ Ready' : '⏳ Loading...'} + + + + {/* Camera Area */} + + + 📷 + + {scanner.isScanning ? '🔴 Scanning...' : '⚪ Ready'} + + + + + {/* Controls */} + + + + {scanner.isScanning ? '⏹ Stop' : '▶ Start'} + + + + + {/* Results */} + {scans.length > 0 && ( + + Results ({scans.length}) + + {scans.map((scan, idx) => ( + + {scan.code} + {scan.format} + + ))} + + + )} + + {/* Error Display */} + {scanner.error && ( + + ⚠ {scanner.error} + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f5f5', + }, + header: { + backgroundColor: '#2c3e50', + padding: 16, + paddingTop: 20, + }, + title: { + fontSize: 24, + fontWeight: '700', + color: '#fff', + }, + subtitle: { + fontSize: 12, + color: '#bdc3c7', + marginTop: 4, + }, + cameraContainer: { + flex: 2, + padding: 16, + }, + cameraPlaceholder: { + flex: 1, + backgroundColor: '#1a1a1a', + borderRadius: 12, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 2, + borderColor: '#444', + }, + cameraIcon: { + fontSize: 60, + marginBottom: 12, + }, + cameraStatus: { + fontSize: 14, + color: '#999', + }, + controls: { + padding: 16, + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#eee', + }, + button: { + paddingVertical: 14, + borderRadius: 8, + alignItems: 'center', + }, + startButton: { + backgroundColor: '#27ae60', + }, + stopButton: { + backgroundColor: '#e74c3c', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + results: { + flex: 1, + padding: 16, + backgroundColor: '#fff', + borderTopWidth: 1, + borderTopColor: '#eee', + }, + resultsTitle: { + fontSize: 14, + fontWeight: '600', + color: '#333', + marginBottom: 8, + }, + resultsList: { + flex: 1, + }, + resultItem: { + paddingVertical: 8, + paddingHorizontal: 12, + marginBottom: 6, + backgroundColor: '#f0f0f0', + borderLeftWidth: 3, + borderLeftColor: '#27ae60', + borderRadius: 4, + }, + resultCode: { + fontSize: 12, + fontWeight: '600', + color: '#000', + }, + resultFormat: { + fontSize: 10, + color: '#666', + marginTop: 2, + }, + error: { + paddingVertical: 10, + paddingHorizontal: 16, + backgroundColor: '#ffe0e0', + borderTopWidth: 1, + borderTopColor: '#ffcccc', + }, + errorText: { + fontSize: 12, + color: '#c00', + fontWeight: '500', + }, +}); diff --git a/src/types/icamera.ts b/src/types/icamera.ts new file mode 100644 index 0000000..60cc4f3 --- /dev/null +++ b/src/types/icamera.ts @@ -0,0 +1,254 @@ +/** + * iCameraSDK TypeScript Types + * Type definitions for the iCameraSDK React Native integration + */ + +export interface ScanResult { + /** + * The scanned code/data + */ + code: string; + + /** + * Format name of the scanned code + * Examples: "QR_CODE", "CODE_128", "CODE_39", "EAN_13", "EAN_8" + */ + format: string; + + /** + * Numeric format code + * QR_CODE: 32, CODE_128: 64, etc. + */ + formatCode: number; +} + +export interface ScannerError { + /** + * Error message + */ + error: string; + + /** + * The error class name + */ + errorType: string; +} + +export interface SDKConfig { + /** + * The presigned URL for uploads + */ + presignedUrl: string; + + /** + * List of enabled features + */ + features: CaptureFeature[]; +} + +export type CaptureFeature = 'QR_CODE' | 'BAR_CODE' | 'CAPTURE_3D' | 'LENS'; + +export interface UseCameraScannerOptions { + /** + * Presigned URL for the SDK + * Should be generated from your backend + */ + presignedUrl: string; + + /** + * Features to enable + * @default ['QR_CODE', 'BAR_CODE'] + */ + features?: CaptureFeature[]; + + /** + * Callback when a code is successfully scanned + */ + onScanResult?: (result: ScanResult) => void; + + /** + * Callback when an error occurs + */ + onError?: (error: string) => void; +} + +export interface UseCameraScannerReturn { + /** + * Whether the SDK is initialized + */ + isInitialized: boolean; + + /** + * Whether scanning is currently active + */ + isScanning: boolean; + + /** + * Current error message, if any + */ + error: string | null; + + /** + * Start the scanner + */ + startScanning: () => Promise; + + /** + * Stop the scanner + */ + stopScanning: () => Promise; + + /** + * Get current SDK configuration + */ + getConfiguration: () => Promise; + + /** + * Clean up and shutdown the SDK + */ + cleanup: () => Promise; +} + +export interface CameraScannerProps { + /** + * Presigned URL for the SDK + */ + presignedUrl: string; + + /** + * Callback when a code is scanned + */ + onScanResult?: (result: ScanResult) => void; + + /** + * Callback when an error occurs + */ + onError?: (error: string) => void; + + /** + * Callback when user closes the scanner + */ + onClose?: () => void; +} + +export interface ScannerScreenProps { + /** + * Presigned URL for the SDK + */ + presignedUrl: string; + + /** + * Callback when a code is scanned + */ + onScan?: (result: ScanResult) => void; + + /** + * Callback when an error occurs + */ + onError?: (error: string) => void; +} + +/** + * Native module interface + * Available as: const { ICameraSDK } = NativeModules; + */ +export interface ICameraSDKModule { + /** + * Initialize the SDK with presigned URL and features + */ + initialize(presignedUrl: string, features: string[]): Promise; + + /** + * Start the scanner + */ + startScanner(): Promise; + + /** + * Stop the scanner + */ + stopScanner(): Promise; + + /** + * Check if SDK is initialized + */ + isInitialized(): Promise; + + /** + * Get current SDK configuration + */ + getConfiguration(): Promise; + + /** + * Shutdown the SDK + */ + shutdown(): Promise; + + /** + * Add event listener (Native Module EventEmitter) + */ + addListener(eventType: string, listener: (...args: any[]) => void): void; + + /** + * Remove event listener + */ + removeListener(eventType: string): void; + + /** + * Remove all listeners + */ + removeAllListeners(): void; +} + +/** + * Barcode format constants + */ +export const BarcodeFormats = { + QR_CODE: 'QR_CODE', + CODE_128: 'CODE_128', + CODE_39: 'CODE_39', + CODE_93: 'CODE_93', + CODABAR: 'CODABAR', + DATA_MATRIX: 'DATA_MATRIX', + EAN_13: 'EAN_13', + EAN_8: 'EAN_8', + ITF: 'ITF', + PDF_417: 'PDF_417', + UPC_A: 'UPC_A', + UPC_E: 'UPC_E', + UNKNOWN: 'UNKNOWN', +} as const; + +export type BarcodeFormat = typeof BarcodeFormats[keyof typeof BarcodeFormats]; + +/** + * Capture features available in the SDK + */ +export const CaptureFeatures = { + QR_CODE: 'QR_CODE' as CaptureFeature, + BAR_CODE: 'BAR_CODE' as CaptureFeature, + CAPTURE_3D: 'CAPTURE_3D' as CaptureFeature, + LENS: 'LENS' as CaptureFeature, +} as const; + +/** + * Event types emitted by the scanner + */ +export const ScannerEvents = { + RESULT: 'onScannerResult', + ERROR: 'onScannerError', +} as const; + +export type ScannerEvent = typeof ScannerEvents[keyof typeof ScannerEvents]; + +/** + * Error types + */ +export enum ScannerErrorType { + InitializationError = 'INIT_ERROR', + ScannerError = 'START_SCANNER_ERROR', + PermissionError = 'PERMISSION_ERROR', + LifecycleError = 'LIFECYCLE_ERROR', + ConfigError = 'CONFIG_ERROR', + NotInitializedError = 'NOT_INITIALIZED', + ShutdownError = 'SHUTDOWN_ERROR', +}