From 9eb141686672c327bc362a9f7111c7b20f4ea2c8 Mon Sep 17 00:00:00 2001 From: yashwin-foxy Date: Thu, 7 Aug 2025 17:22:16 +0530 Subject: [PATCH] upload document type modified --- .../components/signup/DocumentUploadStep.tsx | 41 ++-- .../Auth/screens/ResetPasswordScreen.tsx | 34 ++- app/modules/Auth/screens/SignUpScreen.tsx | 52 ++-- app/modules/Auth/services/authAPI.ts | 13 +- app/shared/utils/fileUpload.ts | 228 ++++++++++++++++++ 5 files changed, 327 insertions(+), 41 deletions(-) create mode 100644 app/shared/utils/fileUpload.ts diff --git a/app/modules/Auth/components/signup/DocumentUploadStep.tsx b/app/modules/Auth/components/signup/DocumentUploadStep.tsx index 81072c7..b06bb9b 100644 --- a/app/modules/Auth/components/signup/DocumentUploadStep.tsx +++ b/app/modules/Auth/components/signup/DocumentUploadStep.tsx @@ -28,6 +28,7 @@ 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 @@ -183,6 +184,17 @@ const DocumentUploadStep: React.FC = ({ 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!'); } @@ -221,6 +233,17 @@ const DocumentUploadStep: React.FC = ({ 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!'); } @@ -231,23 +254,7 @@ const DocumentUploadStep: React.FC = ({ // UTILITY FUNCTIONS // ============================================================================ - /** - * Format File Size - * - * Purpose: Convert bytes to human readable format - * - * @param bytes - File size in bytes - * @returns Formatted file size string - */ - const formatFileSize = (bytes?: number): string => { - if (!bytes) return ''; - - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - if (bytes === 0) return '0 Bytes'; - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; - }; + /** * Get File Type Display diff --git a/app/modules/Auth/screens/ResetPasswordScreen.tsx b/app/modules/Auth/screens/ResetPasswordScreen.tsx index fabaeea..8e51741 100644 --- a/app/modules/Auth/screens/ResetPasswordScreen.tsx +++ b/app/modules/Auth/screens/ResetPasswordScreen.tsx @@ -29,6 +29,7 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { updateOnboarded, logout } from '../redux/authSlice'; import { authAPI } from '../services/authAPI'; import { showError, showSuccess } from '../../../shared/utils/toast'; +import { validateFileType, validateFileSize, prepareFileForUpload } from '../../../shared/utils/fileUpload'; import { theme } from '../../../theme/theme'; import Icon from 'react-native-vector-icons/Feather'; import { AuthNavigationProp } from '../navigation/navigationTypes'; @@ -364,6 +365,17 @@ export const ResetPasswordScreen: React.FC = ({ 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!'); } @@ -402,6 +414,17 @@ export const ResetPasswordScreen: React.FC = ({ 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!'); } @@ -433,12 +456,11 @@ export const ResetPasswordScreen: React.FC = ({ try { const formData = new FormData(); - const file = { - uri: selectedImage.uri, - name: selectedImage.name, - type: selectedImage.type, - }; - formData.append('id_photo', file as any); + + // Prepare file with proper structure using utility function + const preparedFile = prepareFileForUpload(selectedImage, 'id_photo'); + + formData.append('id_photo', preparedFile as any); const response: any = await authAPI.uploadDocument(formData, user?.access_token); console.log('upload response',response) diff --git a/app/modules/Auth/screens/SignUpScreen.tsx b/app/modules/Auth/screens/SignUpScreen.tsx index ef99a87..12e6416 100644 --- a/app/modules/Auth/screens/SignUpScreen.tsx +++ b/app/modules/Auth/screens/SignUpScreen.tsx @@ -40,6 +40,7 @@ import { selectHospitalLoading, selectHospitals } from '../redux/hospitalSelecto import { SignUpData, SignUpStep } from '../types/signup'; import { authAPI } from '../services/authAPI'; import { showError, showSuccess } from '../../../shared/utils/toast'; +import { createFormDataWithFile, validateFileType, validateFileSize } from '../../../shared/utils/fileUpload'; // ============================================================================ // INTERFACES @@ -225,26 +226,45 @@ const SignUpScreen: React.FC = ({ navigation }) => { setIsLoading(true); try { - const formData = new FormData(); let role = 'er_physician'; - formData.append('email', payload.email); - formData.append('password', payload.password); - formData.append('first_name', payload.first_name); - formData.append('last_name', payload.last_name); - formData.append('username', payload.username); - formData.append('dashboard_role', role); - formData.append('hospital_id', payload.hospital_id); - - // Attach file if exists + // Prepare form data with proper file handling + const formFields = { + email: payload.email, + password: payload.password, + first_name: payload.first_name, + last_name: payload.last_name, + username: payload.username, + dashboard_role: role, + hospital_id: payload.hospital_id, + }; + + let formData: FormData; + + // Handle file upload with validation if (payload.id_photo_url) { - const filePath = payload.id_photo_url; - const file = { - uri: filePath, - name: 'id_photo', - type: 'image/jpg', + const fileData = { + uri: payload.id_photo_url, + name: `id_photo_${Date.now()}.jpg`, + type: 'image/jpeg', }; - formData.append('id_photo_url', file as any); + + // Validate file type and size + if (!validateFileType(fileData)) { + showError('Invalid file type. Please select a JPEG, JPG, or PNG image.'); + return; + } + + if (!validateFileSize(fileData, 10)) { + showError('File size too large. Please select an image under 10MB.'); + return; + } + + // Create FormData with file + formData = createFormDataWithFile(formFields, fileData, 'id_photo_url'); + } else { + // Create FormData without file + formData = createFormDataWithFile(formFields); } console.log('payload prepared', formData); diff --git a/app/modules/Auth/services/authAPI.ts b/app/modules/Auth/services/authAPI.ts index 5d2d87f..784bfda 100644 --- a/app/modules/Auth/services/authAPI.ts +++ b/app/modules/Auth/services/authAPI.ts @@ -21,7 +21,11 @@ export const authAPI = { //fetch hospital list gethospitals: () => api.get('/api/hospitals/hospitals/app_user/hospitals', {},buildHeaders()), //user signup - signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData,buildHeaders({ contentType: 'multipart/form-data' })), + signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }), //validate email validatemail: (payload:{email:string}) => api.post('/api/auth/auth/check-email', payload,buildHeaders()), //change password @@ -29,7 +33,12 @@ export const authAPI = { //validate username validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()), //upload document for onboarding - uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData,buildHeaders({token:token, contentType: 'multipart/form-data' })) + uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + ...(token && { 'Authorization': `Bearer ${token}` }), + }, + }) // Add more endpoints as needed }; diff --git a/app/shared/utils/fileUpload.ts b/app/shared/utils/fileUpload.ts new file mode 100644 index 0000000..20e100d --- /dev/null +++ b/app/shared/utils/fileUpload.ts @@ -0,0 +1,228 @@ +/* + * File: fileUpload.ts + * Description: Utility functions for handling file uploads in React Native + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * FileData Interface + * + * Purpose: Defines the structure for file data from image picker + */ +export interface FileData { + uri: string; + name?: string; + type?: string; + size?: number; +} + +/** + * UploadFile Interface + * + * Purpose: Defines the structure for file object in FormData + */ +export interface UploadFile { + uri: string; + name: string; + type: string; +} + +// ============================================================================ +// FILE UPLOAD UTILITIES +// ============================================================================ + +/** + * Prepare File for Upload + * + * Purpose: Convert FileData to proper UploadFile structure for FormData + * + * @param fileData - File data from image picker + * @param fieldName - Form field name for the file + * @returns Properly formatted UploadFile object + */ +export const prepareFileForUpload = ( + fileData: FileData, + fieldName: string = 'file' +): UploadFile => { + // Extract file extension from URI + const uri = fileData.uri; + const fileName = fileData.name || `${fieldName}_${Date.now()}`; + + // Determine file extension and MIME type + let fileExtension = 'jpg'; + let mimeType = 'image/jpeg'; + + if (fileData.type) { + // Use provided type + mimeType = fileData.type; + fileExtension = mimeType.split('/')[1] || 'jpg'; + } else { + // Extract from URI + const uriParts = uri.split('.'); + if (uriParts.length > 1) { + fileExtension = uriParts[uriParts.length - 1].toLowerCase(); + + // Map extension to MIME type + switch (fileExtension) { + case 'png': + mimeType = 'image/png'; + break; + case 'jpeg': + case 'jpg': + mimeType = 'image/jpeg'; + break; + case 'gif': + mimeType = 'image/gif'; + break; + case 'webp': + mimeType = 'image/webp'; + break; + default: + mimeType = 'image/jpeg'; + fileExtension = 'jpg'; + } + } + } + + // Ensure filename has proper extension + const finalFileName = fileName.includes('.') + ? fileName + : `${fileName}.${fileExtension}`; + + return { + uri, + name: finalFileName, + type: mimeType, + }; +}; + +/** + * Create FormData with File + * + * Purpose: Create FormData object with proper file attachment + * + * @param data - Object containing form fields + * @param fileData - File data to upload + * @param fileFieldName - Form field name for the file + * @returns FormData object ready for upload + */ +export const createFormDataWithFile = ( + data: Record, + fileData?: FileData, + fileFieldName: string = 'file' +): FormData => { + const formData = new FormData(); + + // Add regular form fields + Object.keys(data).forEach(key => { + if (data[key] !== null && data[key] !== undefined) { + formData.append(key, String(data[key])); + } + }); + + // Add file if provided + if (fileData) { + const uploadFile = prepareFileForUpload(fileData, fileFieldName); + formData.append(fileFieldName, uploadFile as any); + } + + return formData; +}; + +/** + * Validate File Type + * + * Purpose: Check if file type is allowed + * + * @param fileData - File data to validate + * @param allowedTypes - Array of allowed MIME types + * @returns Boolean indicating if file type is allowed + */ +export const validateFileType = ( + fileData: FileData, + allowedTypes: string[] = ['image/jpeg', 'image/jpg', 'image/png'] +): boolean => { + if (!fileData.type) { + // If no type provided, try to determine from URI + const uri = fileData.uri.toLowerCase(); + if (uri.includes('.jpg') || uri.includes('.jpeg')) { + return allowedTypes.includes('image/jpeg') || allowedTypes.includes('image/jpg'); + } + if (uri.includes('.png')) { + return allowedTypes.includes('image/png'); + } + return false; + } + + return allowedTypes.includes(fileData.type.toLowerCase()); +}; + +/** + * Validate File Size + * + * Purpose: Check if file size is within limits + * + * @param fileData - File data to validate + * @param maxSizeMB - Maximum file size in MB + * @returns Boolean indicating if file size is acceptable + */ +export const validateFileSize = ( + fileData: FileData, + maxSizeMB: number = 10 +): boolean => { + if (!fileData.size) { + return true; // Skip validation if size not available + } + + const maxSizeBytes = maxSizeMB * 1024 * 1024; + return fileData.size <= maxSizeBytes; +}; + +/** + * Format File Size + * + * Purpose: Convert bytes to human readable format + * + * @param bytes - File size in bytes + * @returns Formatted file size string + */ +export const formatFileSize = (bytes?: number): string => { + if (!bytes) return ''; + + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + if (bytes === 0) return '0 Bytes'; + + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; +}; + +/** + * Get File Type Display Name + * + * Purpose: Get user-friendly display name for file type + * + * @param mimeType - MIME type string + * @returns Display name for file type + */ +export const getFileTypeDisplay = (mimeType: string): string => { + const typeMap: Record = { + 'image/jpeg': 'JPEG', + 'image/jpg': 'JPEG', + 'image/png': 'PNG', + 'image/gif': 'GIF', + 'image/webp': 'WebP', + }; + + return typeMap[mimeType.toLowerCase()] || 'Image'; +}; + +/* + * End of File: fileUpload.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ \ No newline at end of file