diff --git a/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java b/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java index ea301ee0..5317d0b0 100644 --- a/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java +++ b/android/app/src/main/java/com/avapp/photoModule/PhotoModule.java @@ -270,7 +270,6 @@ public class PhotoModule extends ReactContextBaseJavaModule { String longitude = attributes.getString("longitude"); exif.setAttribute(ExifInterface.TAG_IMAGE_DESCRIPTION, json.toString()); - exif.setAttribute(ExifInterface.TAG_ARTIST, "Aman Chaturvedi (1932)"); String currentDateTime = getCurrentFormattedDateTime(); exif.setAttribute(ExifInterface.TAG_DATETIME, currentDateTime); diff --git a/src/action/caseApiActions.ts b/src/action/caseApiActions.ts index 8fb50d07..51c56c9c 100644 --- a/src/action/caseApiActions.ts +++ b/src/action/caseApiActions.ts @@ -10,6 +10,8 @@ import { type IDocument, removeDocumentByQuestionKey } from '../reducer/feedback import { addClickstreamEvent } from '@services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '@common/Constants'; import { PAST_FEEDBACK_PAGE_SIZE } from '@screens/caseDetails/feedback/pastFeedbackCommon'; +import RNBlobUtil from 'react-native-blob-util'; +import logger from '@components/utlis/logger'; export const getRepaymentsData = (loanAccountNumber: string, caseId: string, caseBusinessVertical: string) => (dispatch: AppDispatch) => { dispatch(setRepaymentsLoading({ loanAccountNumber, isLoading: true })); @@ -119,54 +121,93 @@ export const getFeedbackHistory = (loanAccountNumber: string) => (dispatch: AppD export const uploadImages = (caseKey: string, documents: Record, interactionReferenceId: string) => - (dispatch: AppDispatch) => { + async (dispatch: AppDispatch) => { if (!documents || !interactionReferenceId) { return; } - _map(documents, (questionKey, index) => { + for (const questionKey of Object.keys(documents)) { const fileDoc = documents[questionKey]; - if (!fileDoc) { - return; + if (!fileDoc || !fileDoc.fileUri) { + continue; } - const { fileUri } = fileDoc; - const formData = new FormData(); - formData.append( - 'originalImageData', - JSON.stringify({ - interactionReferenceId, - questionKey, - }) - ); - formData.append('image', { - uri: fileUri, - name: `image_${interactionReferenceId}_${index}`, - type: 'image/jpeg', - } as any); - const url = getApiUrl(ApiKeys.UPLOAD_FEEDBACK_IMAGES); - axiosInstance - .put(url, formData, { - headers: { - 'Content-Type': 'multipart/form-data', - donotHandleError: true, + const mimeType = 'image/jpeg'; + try { + // Get pre-signed URL + const presignRes = await axiosInstance.post( + getApiUrl(ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE), + { + interactionReferenceId, + questionKey, + mimeType, }, - timeout: 5 * MILLISECONDS_IN_A_MINUTE, - }) - .then((res) => { - dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); - }) - .catch((err) => { - if (err?.response?.status === API_STATUS_CODE.NOT_FOUND) { - dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_WRONG_QUESTION_KEY, { - error: err, - questionKey, - interactionReferenceId, - }); + { + headers: { doNotHandleError: true }, } - addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_FAILURE, { - payload: formData, - }); - logError(err as Error, 'Error uploading image to document service'); + ); + if (!presignRes?.data?.presignedUrl || !presignRes?.data?.id) { + throw new Error('Failed to get presigned URL'); + } + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, }); - }); + + // Upload image to pre-signed URL + const fileStats = await RNBlobUtil.fs.stat(fileDoc.fileUri); + await RNBlobUtil.fetch( + 'PUT', + presignRes?.data?.presignedUrl, + { + 'Content-Type': mimeType, + }, + RNBlobUtil.wrap(fileStats.path) + ); + + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + + // Acknowledge + await axiosInstance.post( + getApiUrl(ApiKeys.FEEDBACK_ORIGINAL_IMAGE_ACK), + {}, + { + params: { id: presignRes?.data?.id }, + headers: { doNotHandleError: true }, + } + ); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + + // Remove document from redux + dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); + } catch (err) { + logger({ + msg: `Image upload failed`, + type: 'error', + extras: { + error: err, + questionKey, + interactionReferenceId, + }, + }); + addClickstreamEvent(CLICKSTREAM_EVENT_NAMES.FA_PERSIST_ORIGINAL_IMAGE_FAILURE, { + payload: { + questionKey, + interactionReferenceId, + }, + }); + continue; + } + } }; diff --git a/src/common/Constants.ts b/src/common/Constants.ts index 6193e1c3..2f23119e 100644 --- a/src/common/Constants.ts +++ b/src/common/Constants.ts @@ -1433,6 +1433,19 @@ export const CLICKSTREAM_EVENT_NAMES = { description: 'Failed to persist original image', }, + FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_PRESIGNED_SUCCESS', + description: 'Successfully presigned generated for original image', + }, + FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_S3_SUCCESS', + description: 'Successfully persisted original image to S3', + }, + FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS: { + name: 'FA_PERSIST_ORIGINAL_IMAGE_ACKNOWLEDGE_SUCCESS', + description: 'Successfully acknowledged original image', + }, + // Filter coachmarks FA_FILTER_COACHMARKS_LOADED: { name: 'FA_FILTER_COACHMARKS_LOADED', diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index f508e4b4..737db3b1 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -120,7 +120,9 @@ export enum ApiKeys { GET_TRAINING_MATERIAL_LIST = 'GET_TRAINING_MATERIAL_LIST', GET_TRAINING_MATERIAL_DETAILS = 'GET_TRAINING_MATERIAL_DETAILS', SELF_CALL_ACK= '/api/v1/self-call', - GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = "GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION" + GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION = "GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION", + GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE = 'GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE', + FEEDBACK_ORIGINAL_IMAGE_ACK = 'FEEDBACK_ORIGINAL_IMAGE_ACK', } export const API_URLS: Record = {} as Record; @@ -230,6 +232,8 @@ API_URLS[ApiKeys.SELF_CALL_ACK] = '/sync-data/self-call-metadata'; API_URLS[ApiKeys.GET_TOP_ADDRESSES] = '/collection-cases/unified-locations'; API_URLS[ApiKeys.GET_FEEDBACK_ADDRESSES] = '/collection-cases/unified-locations/lite'; API_URLS[ApiKeys.GET_CASES_GEOLOCATION_DISTANCE_FROM_AGENT_LOCATION] = '/geolocation-distance/single-source' +API_URLS[ApiKeys.GET_PRE_SIGNED_URL_FOR_FEEDBACK_IMAGE] = '/file-upload/presigned-url'; +API_URLS[ApiKeys.FEEDBACK_ORIGINAL_IMAGE_ACK] = '/file-upload/acknowledge'; export const API_STATUS_CODE = { OK: 200,