diff --git a/src/action/dataActions.ts b/src/action/dataActions.ts index 4ab0f11d..74ac6864 100644 --- a/src/action/dataActions.ts +++ b/src/action/dataActions.ts @@ -105,7 +105,6 @@ export const syncCaseDetail = offlineCaseKey: payload.data.offlineCaseKey, }) ); - OfflineImageDAO.deleteImages(offlineImageIdList); toast({ type: 'success', text1: ToastMessages.FEEDBACK_SUCCESSFUL, @@ -117,10 +116,15 @@ export const syncCaseDetail = .catch((errObj) => { const statusCode = errObj?.response?.status; const errorCode = errObj?.response?.data?.error_code; + const errorMessage = errObj?.response?.data?.message; + + const isImageNotFoundError = errorMessage?.includes('base64'); if (isAxiosError(errObj) && statusCode !== API_STATUS_CODE.UNPROCESSABLE_CONTENT) { toast({ type: 'error', - text1: ToastMessages.FEEDBACK_FAILED, + text1: isImageNotFoundError + ? ToastMessages.FEEDBACK_IMAGE_NOT_FOUND + : ToastMessages.FEEDBACK_FAILED, }); } if (callbacks?.onErrorCB != null && typeof callbacks?.onErrorCB === 'function') { @@ -186,11 +190,11 @@ export const getSignedApi = async ( return new Promise((res) => { if (shouldBatch) { batchSignedApiRequest(signedRequestPayload, (results: any) => { - res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' }); + res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' }); }, skipFirebaseUpdate); } else { makeBulkSignedApiRequest(signedRequestPayload, (results: any) => { - res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' }); + res({ imageUrl: results?.[signedRequestPayload[0].documentReferenceId] || '' }); }, skipFirebaseUpdate); } }); diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 496d473b..d91ed9a4 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -1294,6 +1294,14 @@ export const CLICKSTREAM_EVENT_NAMES = { description: 'Feedback nudge closed' }, + FA_FEEDBACK_IMAGE_NOT_FOUND: { + name: 'FA_FEEDBACK_IMAGE_NOT_FOUND', + description: 'Feedback image not found' + }, + FA_UNSYNC_FEEDBACK_CAPTURED: { + name: 'FA_UNSYNC_FEEDBACK_CAPTURED', + description: 'Unsync feedback captured' + }, } as const; export enum MimeType { diff --git a/src/components/form/components/AddressSelection.tsx b/src/components/form/components/AddressSelection.tsx index 25723745..c6e3826e 100644 --- a/src/components/form/components/AddressSelection.tsx +++ b/src/components/form/components/AddressSelection.tsx @@ -62,7 +62,7 @@ const AddressSelection: React.FC = (props) => { ); const caseType = currentCase?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE; const template = useAppSelector((state) => state.case.templateData[caseType]); - const question = template.questions[questionId]; + const question = template?.questions?.[questionId]; const dispatch = useAppDispatch(); diff --git a/src/components/form/components/ImageUpload.tsx b/src/components/form/components/ImageUpload.tsx deleted file mode 100644 index 34e2eba0..00000000 --- a/src/components/form/components/ImageUpload.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Control, Controller } from 'react-hook-form'; -import { - ActivityIndicator, - ImageBackground, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native'; -import PhotoUpload, { - IImageDetails, - LAUNCH_REQUEST, -} from '../../../../RN-UI-LIB/src/components/photoUpload/PhotoUpload'; -import { useSelector } from 'react-redux'; -import Text from '../../../../RN-UI-LIB/src/components/Text'; -import DeleteIcon from '../../../../RN-UI-LIB/src/Icons/DeleteIcon'; -import { GenericStyles } from '../../../../RN-UI-LIB/src/styles'; -import { COLORS } from '../../../../RN-UI-LIB/src/styles/colors'; -import { useAppDispatch, useAppSelector } from '../../../hooks'; -import { RootState } from '../../../store/store'; -import { AnswerType } from '../interface'; -import ErrorMessage from './ErrorMessage'; -import withObservables from '@nozbe/with-observables'; -import OfflineImageDAO from '../../../wmDB/dao/OfflineImageDAO'; -import { - CLICKSTREAM_EVENT_NAMES, - PrefixJpegBase64Image, - getPrefixBase64Image, -} from '../../../common/Constants'; -import { addClickstreamEvent } from '../../../services/clickstreamEventService'; -import { isQuestionMandatory, validateInput } from '../services/validation.service'; -import { CaseAllocationType } from '../../../screens/allCases/interface'; -import { - addIntermediateDocument, - deleteIntermediateDocument, -} from '../../../reducer/feedbackImagesSlice'; - -interface IOfflineImage { - idx: string; - imageData: string; - originalImageUri: string; -} - -interface IImageUpload { - questionType: string; - questionId: string; - widgetId: string; - journeyId: string; - caseId: string; - sectionId: string; - control: Control; - error: any; - offlineImages: IOfflineImage[]; -} - -const getImageUri = (imageList: IOfflineImage[], imageId: string) => { - const image = imageList?.find((image) => image?.idx === imageId); - let uri = ''; - if (image) { - const { originalImageUri, imageData } = image || {}; - uri = originalImageUri || imageData || ''; - } - return uri; -}; - -const ImageUpload: React.FC = (props) => { - const { questionId, error, sectionId, caseId, journeyId, widgetId, questionType } = props; - const [imageId, setImageId] = useState(''); - const [loading, setImageLoading] = useState(false); - const caseType = useAppSelector( - (state) => - state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE - ); - const template = useAppSelector((state) => state.case.templateData[caseType]); - const question = template.questions[questionId as keyof typeof template.questions]; - const dataFromRedux = useSelector( - (state: RootState) => - state.case.caseForm?.[caseId]?.[journeyId]?.widgetContext?.[widgetId]?.sectionContext?.[ - sectionId - ]?.questionContext?.[questionId]?.answer - ); - - const dispatch = useAppDispatch(); - - useEffect(() => { - if (dataFromRedux) { - setImageId(dataFromRedux); - } - }, [dataFromRedux]); - - if (!question) { - return null; - } - - const addOriginalFileUriToDocs = (caseId: string, fileUri: string, questionKey: string) => { - if (!fileUri) { - return; - } - dispatch(addIntermediateDocument({ caseId, fileUri, questionKey })); - }; - - const addImageUploadSuccessClickstream = (openRequest: LAUNCH_REQUEST) => { - if (openRequest === LAUNCH_REQUEST.CAMERA) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FEEDBACK_PHOTO_CAPTURED_SUCCESSFULLY, { - caseId, - questionType, - questionId, - sectionId, - widgetId, - }); - } else if (openRequest === LAUNCH_REQUEST.IMAGE_LIBRARY) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FEEDBACK_PHOTO_UPLOADED_SUCCESSFULLY, { - caseId, - questionType, - questionId, - sectionId, - widgetId, - }); - } - }; - - const handleChange = async (clickedImage: IImageDetails, onChange: (...event: any[]) => void) => { - const { base64 = '', uri = '', openRequest } = clickedImage; - addImageUploadSuccessClickstream(openRequest); - addOriginalFileUriToDocs(caseId, uri, questionId); - if (!base64) { - return; - } - const base64Image = `${PrefixJpegBase64Image}${base64}`; - var uniqueId = 'id' + new Date().getTime(); - setImageId(uniqueId); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_ELEMENT_CHANGED, { - caseId, - questionType, - value: uniqueId, - questionId, - error, - sectionId, - widgetId, - }); - onChange({ - answer: uniqueId, - type: AnswerType.image, - }); - await OfflineImageDAO.addImage(base64Image, uri, uniqueId); - }; - - const handleImageDelete = () => { - setImageId(''); - dispatch(deleteIntermediateDocument({ caseId, questionKey: questionId })); - }; - - const handleError = (error: string) => { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PHOTO_UPLOAD_ERROR, { - caseId, - questionType, - questionId, - error, - sectionId, - widgetId, - }); - }; - - const handlePictureAction = (openRequest: LAUNCH_REQUEST) => { - if (openRequest === LAUNCH_REQUEST.CAMERA) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FEEDBACK_CLICKED_TAKE_PHOTO, { - caseId, - questionType, - questionId, - sectionId, - widgetId, - }); - } else if (openRequest === LAUNCH_REQUEST.IMAGE_LIBRARY) { - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_FEEDBACK_CLICKED_UPLOAD_PHOTO, { - caseId, - questionType, - questionId, - sectionId, - widgetId, - }); - } - }; - - // TODO : add the validator back when firestore is fixed. - return ( - - - {question.text}{' '} - {isQuestionMandatory(question) && *} - - {!imageId ? ( - validateInput(data, question.metadata.validators) }} - render={({ field: { onChange } }) => ( - handleChange(clickedImage, onChange)} - showUploadFromGalleryOption={true} - containerStyle={[GenericStyles.containerStyle, loading ? styles.displayNone : null]} - imageLoading={setImageLoading} - onError={handleError} - /> - )} - name={`widgetContext.${widgetId}.sectionContext.${props.sectionId}.questionContext.${questionId}`} - /> - ) : ( - - handleError('Error in image rendering')} - // onLoadStart={() => setImageLoading(true)} - // onLoadEnd={() => setImageLoading(false)} - > - - - - {loading ? ( - - ) : null} - - - )} - - - - - - ); -}; - -const styles = StyleSheet.create({ - image: { - width: '100%', - height: 350, - resizeMode: 'contain', - }, - deleteButton: { - position: 'absolute', - right: 8, - top: 8, - backgroundColor: COLORS.BACKGROUND.PRIMARY, - height: 28, - width: 28, - borderRadius: 14, - justifyContent: 'center', - alignItems: 'center', - borderWidth: 1, - borderColor: COLORS.BORDER.PRIMARY, - }, - br8: { - borderRadius: 8, - }, - displayNone: { - display: 'none', - }, - loadingState: { - justifyContent: 'center', - backgroundColor: COLORS.BACKGROUND.PRIMARY, - alignItems: 'center', - borderRadius: 8, - }, - centerAllign: { - position: 'absolute', - top: '50%', - left: '50%', - }, -}); -const enhance = withObservables([], () => ({ - offlineImages: OfflineImageDAO.observeOfflineImage(), -})); - -export default enhance(ImageUpload); diff --git a/src/components/form/components/imageUpload/ImageUploadV2.tsx b/src/components/form/components/imageUpload/ImageUploadV2.tsx index fc4d1e05..0b95c9bf 100644 --- a/src/components/form/components/imageUpload/ImageUploadV2.tsx +++ b/src/components/form/components/imageUpload/ImageUploadV2.tsx @@ -205,7 +205,6 @@ const ImageUploadV2: React.FC = (props) => { answer: uniqueId, type: AnswerType.image, }); - await OfflineImageDAO.addImage(base64Image, uri, uniqueId, imageWidth, imageHeight); toast({ type: 'success', text1: 'Geolocation & Timestamp added successfully' }); setImageLoading(false); addOriginalFileUriToDocs(caseId, uri, questionId, imageWidth, imageHeight); @@ -221,11 +220,7 @@ const ImageUploadV2: React.FC = (props) => { } }; - const { - fileUri, - imageHeight = 350, - imageWidth = 350 - } = imageDoc || {}; + const { fileUri, imageHeight = 350, imageWidth = 350 } = imageDoc || {}; const imageHeightWrtAspectRatio = getImageHeightWrtAspectRatio( imageWidth, @@ -242,13 +237,12 @@ const ImageUploadV2: React.FC = (props) => { ) => { setImageLoading(false); const { base64 = '', uri = '', openRequest } = clickedImage; + const uniqueId = 'id' + new Date().getTime(); addImageUploadSuccessClickstream(openRequest); - addOriginalFileUriToDocs(caseId, uri, questionId); if (!base64) { return; } const base64Image = `${PrefixJpegBase64Image}${base64}`; - var uniqueId = 'id' + new Date().getTime(); setImageId(uniqueId); addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.AV_FORM_ELEMENT_CHANGED, { caseId, @@ -263,7 +257,7 @@ const ImageUploadV2: React.FC = (props) => { answer: uniqueId, type: AnswerType.image, }); - await OfflineImageDAO.addImage(base64Image, uri, uniqueId); + addOriginalFileUriToDocs(caseId, uri, questionId, imageWidth, imageHeight); }; const handleImageUpload = async (onChange: (...event: any[]) => void) => { diff --git a/src/components/form/services/formComponents.ts b/src/components/form/services/formComponents.ts index 5fe57997..553c17f1 100644 --- a/src/components/form/services/formComponents.ts +++ b/src/components/form/services/formComponents.ts @@ -10,7 +10,6 @@ import AddressSelection from '../components/AddressSelection'; import PhoneNumberSelection from '../components/PhoneNumberSelection'; import DateInput from '../components/DateInput'; import TimeInput from '../components/TimeInput'; -import ImageUpload from '../components/ImageUpload'; export const FormComponentList = { TextInput, diff --git a/src/screens/allCases/constants.ts b/src/screens/allCases/constants.ts index 5e226b94..be787421 100644 --- a/src/screens/allCases/constants.ts +++ b/src/screens/allCases/constants.ts @@ -64,6 +64,7 @@ export const ToastMessages = { SUCCESS_COPYING_EMPLOYER_NAME: 'Employer Name Copied Successfully!!', FEEDBACK_SUCCESSFUL: 'Feedback submitted successfully!', FEEDBACK_FAILED: 'Feedback submission failed', + FEEDBACK_IMAGE_NOT_FOUND: 'Feedback submission failed. Please try uploading image again', FIRESTORE_SIGNIN_FAILED: 'Error signing in to Firestore', PAYMENT_LINK_ERROR: 'Payment link could not be shared', PAYMENT_LINK_SUCCESS: 'Link has been generated and shared with customer', diff --git a/src/screens/caseDetails/interactionsHandler.tsx b/src/screens/caseDetails/interactionsHandler.tsx index ee00eada..bfb5cf5b 100644 --- a/src/screens/caseDetails/interactionsHandler.tsx +++ b/src/screens/caseDetails/interactionsHandler.tsx @@ -4,10 +4,7 @@ import { syncCaseDetail } from '../../action/dataActions'; import { CLICKSTREAM_EVENT_NAMES } from '../../common/Constants'; import { useAppDispatch, useAppSelector } from '../../hooks'; import useIsOnline from '../../hooks/useIsOnline'; -import { - getTransformedAvCase, - getTransformedCollectionCaseItem, -} from '../../services/casePayload.transformer'; +import { getTransformedCollectionCaseItem } from '../../services/casePayload.transformer'; import { addClickstreamEvent } from '../../services/clickstreamEventService'; import { CaseAllocationType } from '../allCases/interface'; import { CaseDetail } from './interface'; @@ -64,12 +61,13 @@ const interactionsHandler = () => { for (const caseItem of notSyncedCases) { if (isOnline) { const caseKey = caseItem.offlineCaseKey || caseItem.id; + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_UNSYNC_FEEDBACK_CAPTURED, { + caseId: caseKey, + }); inProgressCaseIds.current.push(caseKey); let modifiedCaseItem: any; if (caseItem?.caseType === CaseAllocationType.COLLECTION_CASE) { modifiedCaseItem = await getTransformedCollectionCaseItem(caseItem, true); - } else { - modifiedCaseItem = await getTransformedAvCase(caseItem, templateId); } dispatch( syncCaseDetail(modifiedCaseItem, { diff --git a/src/services/casePayload.transformer.ts b/src/services/casePayload.transformer.ts index 4bf17836..0631de29 100644 --- a/src/services/casePayload.transformer.ts +++ b/src/services/casePayload.transformer.ts @@ -1,16 +1,13 @@ -import { CONTEXT_TASK_STATUSES, CaseDetail } from './../screens/caseDetails/interface'; +import { CaseDetail } from './../screens/caseDetails/interface'; import { AnswerType } from '../components/form/interface'; import OfflineImageDAO from '../wmDB/dao/OfflineImageDAO'; -import { - CaseAllocationType, - IAvCasePayload, - IAvTaskFeedbackItem, - IQuestionContextOutput, - TaskTitle, -} from '../screens/allCases/interface'; +import { IQuestionContextOutput } from '../screens/allCases/interface'; import Geolocation from 'react-native-geolocation-service'; - -const AV_TEMPLATE_VERSION_NUMBER = 3; +import store from '@store'; +import RNFS from 'react-native-fs'; +import { addClickstreamEvent } from './clickstreamEventService'; +import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; +import ImageResizer from '@bam.tech/react-native-image-resizer'; interface QuestionContext { answer: string; @@ -36,8 +33,17 @@ interface Answer { allocationReferenceId: string; } -export const extractQuestionContext = async (answer: Answer): Promise => { +const IMAGE_QUALITY = 50; +const MAX_WIDTH = 500; +const MAX_HEIGHT = 500; + +export const extractQuestionContext = async ( + answer: Answer, + caseReferenceId: string +): Promise => { const questionContexts: IQuestionContextOutput[] = []; + const docsData = + store?.getState()?.feedbackImages?.intermediateDocsToBeUploaded?.[caseReferenceId]?.documents; const { widgetContext } = answer; for (const widgetKey in widgetContext) { @@ -64,12 +70,26 @@ export const extractQuestionContext = async (answer: Answer): Promise { - let imageList = await OfflineImageDAO.getImage(imageId); - // returns the latest image in the database - return imageList?.[0]; -} - -export const getImageHeightWrtAspectRatio = (imageWidth: number, imageHeight: number, requiredImageWidth: number) => { +export const getImageHeightWrtAspectRatio = ( + imageWidth: number, + imageHeight: number, + requiredImageWidth: number +) => { if (!imageWidth || !imageHeight) { return 0; } @@ -101,11 +119,6 @@ export const getBase64ImageFromOfflineDb = async (imageId: string) => { return imageList?.[0]?.imageData; }; -export const getOriginalImageFromOfflineDb = async (imageId: string) => { - let imageList = await OfflineImageDAO.getImage(imageId); - return imageList?.[0]?.originalImageUri; -}; - export interface IGetTransformedCaseItem extends CaseDetail { answer: any; caseId: string; @@ -118,7 +131,10 @@ export const getTransformedCollectionCaseItem = async ( forceSubmit = false ) => { let cloneCaseItem = { ...caseItem }; - let answerContextArray = await extractQuestionContext(cloneCaseItem?.answer); + let answerContextArray = await extractQuestionContext( + cloneCaseItem?.answer, + caseItem?.caseReferenceId + ); let data = { caseReferenceId: caseItem.caseReferenceId, answers: answerContextArray, @@ -129,42 +145,3 @@ export const getTransformedCollectionCaseItem = async ( }; return { caseType: caseItem.caseType, data, forceSubmit }; }; - -export const getTransformedAvCase = async ( - caseItem: IGetTransformedCaseItem, - templateId: string -) => { - const { caseType, allocationReferenceId, caseId, coords } = caseItem; - const transformedAvCase: IAvCasePayload = { - caseType: caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE, - data: { - version: AV_TEMPLATE_VERSION_NUMBER, - templateId, - allocationReferenceId: allocationReferenceId as string, - taskFeedbacks: [], - }, - }; - const taskContext = caseItem?.context?.taskContext; - for (let taskId in taskContext) { - const taskItems: any[] = taskContext[taskId]; - for (const taskItem of taskItems) { - const { taskStatus, createdAt } = taskItem; - if (taskId !== TaskTitle.CALLING_TASK && taskStatus === CONTEXT_TASK_STATUSES.OPEN) continue; - - let taskFeedbackItem: IAvTaskFeedbackItem = { - taskType: taskId as TaskTitle, - location: coords, - answers: [], - taskStatus, - capturedAt: createdAt || new Date().getTime(), - }; - - const answersList = await extractQuestionContext(taskItem); - if (!answersList.length) continue; - - taskFeedbackItem.answers = answersList; - transformedAvCase.data.taskFeedbacks.push(taskFeedbackItem); - } - } - return transformedAvCase; -};