NeoScan_Physician/app/modules/Auth/components/signup/DocumentUploadStep.tsx
2025-08-20 20:42:33 +05:30

641 lines
17 KiB
TypeScript

/*
* File: DocumentUploadStep.tsx
* Description: Document upload step component for signup flow with image picker
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
Alert,
PermissionsAndroid,
Platform,
KeyboardAvoidingView,
} from 'react-native';
import {
launchImageLibrary,
launchCamera,
ImagePickerResponse,
MediaType,
} from 'react-native-image-picker';
import { theme } from '../../../../theme/theme';
import { DocumentUploadStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
import { showError, showSuccess } from '../../../../shared/utils/toast';
import { validateFileType, validateFileSize, formatFileSize } from '../../../../shared/utils/fileUpload';
// ============================================================================
// INTERFACES
// ============================================================================
/**
* ImageData Interface
*
* Purpose: Defines the structure for image data
*/
interface ImageData {
uri: string;
name: string;
type: string;
size?: number;
}
// ============================================================================
// DOCUMENT UPLOAD STEP COMPONENT
// ============================================================================
/**
* DocumentUploadStep Component
*
* Purpose: Fourth step of signup flow - document upload with image picker
*
* Features:
* - Camera and gallery image selection
* - Image preview with file details
* - Real-time file size and type display
* - Permission handling for camera
* - Modern UI with proper header alignment
* - Continue button with loading state
* - Back navigation
*/
const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [selectedImage, setSelectedImage] = useState<ImageData | null>(
data.id_photo_url ? {
uri: data.id_photo_url,
name: 'uploaded_document.jpg',
type: 'image/jpeg',
} : null
);
// ============================================================================
// PERMISSION HANDLERS
// ============================================================================
/**
* Request Camera Permission
*
* Purpose: Request camera permission for Android devices
*
* @returns Promise<boolean> - Whether permission was granted
*/
const requestCameraPermission = async (): Promise<boolean> => {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
{
title: 'Camera Permission',
message: 'This app needs camera permission to capture images.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn('Camera permission error:', err);
return false;
}
}
return true;
};
// ============================================================================
// IMAGE PICKER HANDLERS
// ============================================================================
/**
* Handle Image Picker Selection
*
* Purpose: Show options for camera or gallery selection
*/
const handleImagePicker = () => {
Alert.alert(
'Select Image',
'Choose how you want to upload your document',
[
{
text: 'Camera',
onPress: () => handleCameraCapture(),
},
{
text: 'Gallery',
onPress: () => handleGalleryPicker(),
},
{
text: 'Cancel',
style: 'cancel',
},
]
);
};
/**
* Handle Camera Capture
*
* Purpose: Launch camera to capture image
*/
const handleCameraCapture = async () => {
const hasPermission = await requestCameraPermission();
if (!hasPermission) {
showError('Permission Error', 'Camera permission is required to capture images.');
return;
}
const options = {
mediaType: 'photo' as MediaType,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
};
launchCamera(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return;
}
if (response.errorMessage) {
showError('Camera Error', response.errorMessage);
return;
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0];
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `document_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document captured successfully!');
}
});
};
/**
* Handle Gallery Picker
*
* Purpose: Launch image library to select image
*/
const handleGalleryPicker = () => {
const options = {
mediaType: 'photo' as MediaType,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
};
launchImageLibrary(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return;
}
if (response.errorMessage) {
showError('Gallery Error', response.errorMessage);
return;
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0];
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `document_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document selected from gallery!');
}
});
};
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Get File Type Display
*
* Purpose: Get display name for file type
*
* @param type - MIME type
* @returns Display name for file type
*/
const getFileTypeDisplay = (type: string): string => {
if (type.includes('jpeg') || type.includes('jpg')) return 'JPEG';
if (type.includes('png')) return 'PNG';
if (type.includes('gif')) return 'GIF';
if (type.includes('webp')) return 'WebP';
return 'Image';
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Continue
*
* Purpose: Proceed to next step with selected image
*/
const handleContinue = () => {
if (!selectedImage) {
showError('Validation Error', 'Please upload a document to continue.');
return;
}
onContinue(selectedImage.uri);
};
/**
* Handle Remove Image
*
* Purpose: Remove selected image
*/
const handleRemoveImage = () => {
setSelectedImage(null);
};
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Upload Document</Text>
<Text style={styles.subtitle}>Step 4 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Upload your ID document</Text>
<Text style={styles.description}>
Please upload a clear photo of your hospital-issued ID for verification.
</Text>
{/* Document Upload Area */}
<TouchableOpacity
style={styles.uploadContainer}
onPress={handleImagePicker}
disabled={isLoading}
>
<View style={styles.uploadContent}>
{selectedImage ? (
<View style={styles.imagePreviewContainer}>
<Image source={{ uri: selectedImage.uri }} style={styles.imagePreview} />
<TouchableOpacity
style={styles.imageOverlay}
onPress={handleRemoveImage}
>
<Icon name="x" size={20} color={theme.colors.background} />
</TouchableOpacity>
<Text style={styles.uploadedText}>Document Uploaded</Text>
<Text style={styles.fileName}>{selectedImage.name}</Text>
<View style={styles.fileDetails}>
<Text style={styles.fileType}>{getFileTypeDisplay(selectedImage.type)}</Text>
{selectedImage.size && (
<Text style={styles.fileSize}> {formatFileSize(selectedImage.size)}</Text>
)}
</View>
</View>
) : (
<>
<Icon name="image" size={48} color={theme.colors.textSecondary} />
<Text style={styles.uploadText}>Tap to upload document</Text>
<Text style={styles.uploadSubtext}>JPG, PNG supported</Text>
</>
)}
</View>
</TouchableOpacity>
{selectedImage && (
<TouchableOpacity
style={styles.changeButton}
onPress={handleImagePicker}
disabled={isLoading}
>
<Text style={styles.changeButtonText}>Change Document</Text>
</TouchableOpacity>
)}
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!selectedImage || isLoading) && styles.continueButtonDisabled,
]}
onPress={handleContinue}
disabled={!selectedImage || isLoading}
>
<Text style={[
styles.continueButtonText,
(!selectedImage || isLoading) && styles.continueButtonTextDisabled,
]}>
{isLoading ? 'Processing...' : 'Continue'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Upload container
uploadContainer: {
backgroundColor: theme.colors.backgroundAlt,
borderWidth: 2,
borderColor: theme.colors.border,
borderStyle: 'dashed',
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.xl,
marginBottom: theme.spacing.md,
minHeight: 200,
},
// Upload content
uploadContent: {
alignItems: 'center',
},
// Image preview container
imagePreviewContainer: {
alignItems: 'center',
width: '100%',
},
// Image preview
imagePreview: {
width: '100%',
height: 150,
borderRadius: theme.borderRadius.medium,
marginBottom: theme.spacing.sm,
resizeMode: 'contain',
},
// Image overlay (remove button)
imageOverlay: {
position: 'absolute',
top: 0,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
borderRadius: 17,
width: 34,
height: 34,
justifyContent: 'center',
alignItems: 'center',
},
// Upload text
uploadText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginTop: theme.spacing.sm,
},
// Upload subtext
uploadSubtext: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.xs,
},
// Uploaded text
uploadedText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.success,
marginTop: theme.spacing.sm,
},
// File name
fileName: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.xs,
textAlign: 'center',
maxWidth: '80%',
},
// File details
fileDetails: {
flexDirection: 'row',
alignItems: 'center',
marginTop: theme.spacing.xs,
},
// File type
fileType: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// File size
fileSize: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// Change button
changeButton: {
alignSelf: 'center',
marginBottom: theme.spacing.xl,
},
// Change button text
changeButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default DocumentUploadStep;
/*
* End of File: DocumentUploadStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/