diff --git a/RN-UI-LIB b/RN-UI-LIB index 294011df..a1b7dac2 160000 --- a/RN-UI-LIB +++ b/RN-UI-LIB @@ -1 +1 @@ -Subproject commit 294011df80e6ab791b258a07e36594cc3dff3499 +Subproject commit a1b7dac28345c56225bb7972b805b1bd05bf41df diff --git a/package.json b/package.json index c0b74a84..800326e5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "prepare": "husky install" }, "dependencies": { + "@bam.tech/react-native-image-resizer": "3.0.5", "@cobo/apm-rum-react-native": "^0.6.0", "@elastic/apm-rum-core": "^5.17.0", "@nozbe/watermelondb": "0.24.0", diff --git a/src/action/caseApiActions.ts b/src/action/caseApiActions.ts index ab02ee85..4ec000ab 100644 --- a/src/action/caseApiActions.ts +++ b/src/action/caseApiActions.ts @@ -12,6 +12,9 @@ import { GenericType } from '../common/GenericTypes'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants'; import { PageRouteEnum } from '../screens/auth/ProtectedRouter'; +import { MILLISECONDS_IN_A_MINUTE, _map } from '../../RN-UI-LIB/src/utlis/common'; +import { logError } from '../components/utlis/errorUtils'; +import { IDocument, removeDocumentByQuestionKey } from '../reducer/feedbackImagesSlice'; // TODO: Need to add respective interfaces instead of any interface IUnifiedData { @@ -21,6 +24,12 @@ interface IUnifiedData { repayments: Array; } +export interface IUploadImagePayload { + interactionId: string; + questionKey: string; + originalImageDocumentReferenceId: string; +} + export enum UnifiedCaseDetailsTypes { ADDRESS_AND_GEOLOCATIONS = 'includeAddressesAndGeoLocations', FEEDBACKS = 'includeFeedbacks', @@ -135,3 +144,46 @@ export const getCaseUnifiedData = dispatch(setAddressLoading({ isLoading: false, loanAccountNumbers })); }); }; + +export const uploadImages = + (caseKey: string, documents: Record, interactionReferenceId: string) => + (dispatch: AppDispatch) => { + if (!documents || !interactionReferenceId) { + return; + } + _map(documents, (questionKey, index) => { + const fileDoc = documents[questionKey]; + if (!fileDoc) { + return; + } + 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, + }, + timeout: 5 * MILLISECONDS_IN_A_MINUTE, + }) + .then((res) => { + dispatch(removeDocumentByQuestionKey({ caseKey, questionKey })); + }) + .catch((err) => { + logError(err as Error, 'Error uploading image to document service'); + }); + }); + }; diff --git a/src/action/dataActions.ts b/src/action/dataActions.ts index 43db1e5b..98498536 100644 --- a/src/action/dataActions.ts +++ b/src/action/dataActions.ts @@ -7,15 +7,9 @@ import { resetTodoList, setLoading, setVisitPlansUpdating, - updateCaseDetail, updateSingleCase, } from '../reducer/allCasesSlice'; -import { - CaseAllocationType, - CaseType, - ICaseItem, - IPinnedCasesPayload, -} from '../screens/allCases/interface'; +import { CaseAllocationType, ICaseItem, IPinnedCasesPayload } from '../screens/allCases/interface'; import { AppDispatch } from '../store/store'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { CLICKSTREAM_EVENT_NAMES } from '../common/Constants'; @@ -70,12 +64,10 @@ export const postPinnedList = export const syncCaseDetail = ( payload: any, - updatedCaseDetail?: any, callbacks?: { - onSuccessCB?: (data: any, actions?: any) => void; + onSuccessCB?: (data: any, actions?: any, interactionId?: string) => void; onErrorCB?: (e: Error) => void; - }, - nextActions?: any + } ) => (dispatch: AppDispatch) => { const offlineImageIdList = getOfflineImageId(payload); @@ -88,6 +80,7 @@ export const syncCaseDetail = }) .then((res) => { const caseType = payload.caseType; + const interactionId = res.data?.referenceId; dispatch( updateSingleCase({ data: res.data, @@ -102,7 +95,7 @@ export const syncCaseDetail = text1: ToastMessages.FEEDBACK_SUCCESSFUL, }); if (callbacks?.onSuccessCB != null && typeof callbacks?.onSuccessCB === 'function') { - callbacks?.onSuccessCB(payload.data.answers, nextActions); + callbacks?.onSuccessCB(payload.data.answers, interactionId); } }) .catch((e) => { @@ -111,14 +104,6 @@ export const syncCaseDetail = type: 'error', text1: ToastMessages.FEEDBACK_FAILED, }); - if (updatedCaseDetail) { - dispatch( - updateCaseDetail({ - caseId: payload.data.caseReferenceId, - updatedCaseDetail, - }) - ); - } } if (callbacks?.onErrorCB != null && typeof callbacks?.onErrorCB === 'function') { callbacks?.onErrorCB(e); diff --git a/src/components/expandableImage/ExpandableImage.tsx b/src/components/expandableImage/ExpandableImage.tsx index eb547e67..7abbd4c4 100644 --- a/src/components/expandableImage/ExpandableImage.tsx +++ b/src/components/expandableImage/ExpandableImage.tsx @@ -8,9 +8,10 @@ interface IExpandableImage { imageSrc?: string; close?: () => void; title?: string; + fallbackImage?: string; } -const imageHtml = (imageSrc?: string) => ` +const imageHtml = (imageSrc?: string, fallbackImage?: string) => ` Image Viewer @@ -24,19 +25,37 @@ const imageHtml = (imageSrc?: string) => ` justify-content: center; align-items: center; } + .parent { + position: relative; + top: 0; + left: 0; + } + .image1 { + position: relative; + top: 0; + left: 0; + width: calc(100vw - 32px); + max-height: calc(100vh - 32px) + } + .image2 { + position: absolute; + top: 0; + left: 0; + width: calc(100vw - 32px); + max-height: calc(100vh - 32px) + } -
-
- -
-
+
+ + +
`; -const ExpandableImage: React.FC = ({ imageSrc, title, close }) => { +const ExpandableImage: React.FC = ({ imageSrc, fallbackImage, title, close }) => { return ( @@ -44,7 +63,7 @@ const ExpandableImage: React.FC = ({ imageSrc, title, close }) scalesPageToFit={true} bounces={false} scrollEnabled={false} - source={{ html: imageHtml(imageSrc) }} + source={{ html: imageHtml(imageSrc, fallbackImage) }} /> ); diff --git a/src/components/form/AnswerRender.tsx b/src/components/form/AnswerRender.tsx index 0d37b8ff..cb022019 100644 --- a/src/components/form/AnswerRender.tsx +++ b/src/components/form/AnswerRender.tsx @@ -8,7 +8,7 @@ import { GenericObject, GenericType } from '../../common/GenericTypes'; import StarRating from '../../../RN-UI-LIB/src/components/star_rating/StarRating'; import { CaseAllocationType } from '../../screens/allCases/interface'; import { getAddressString, getPhoneNumberString, memoize } from '../utlis/commonFunctions'; -import { getImageFromOfflineDb } from '../../services/casePayload.transformer'; +import { getBase64ImageFromOfflineDb } from '../../services/casePayload.transformer'; import { formatAmount } from '../../../RN-UI-LIB/src/utlis/amount'; const RATING_COMPONENT = 'Rating'; @@ -61,7 +61,7 @@ const AnswerRender: React.FC = (props) => { React.useEffect(() => { if (answer?.type === AnswerType.image) { (async () => { - const data = await getImageFromOfflineDb(answer.answer); + const data = await getBase64ImageFromOfflineDb(answer.answer); setImageUrl(data); })(); } diff --git a/src/components/form/components/ImageUpload.tsx b/src/components/form/components/ImageUpload.tsx index 14ab58c1..5657f824 100644 --- a/src/components/form/components/ImageUpload.tsx +++ b/src/components/form/components/ImageUpload.tsx @@ -1,26 +1,37 @@ import React, { useEffect, useState } from 'react'; import { Control, Controller } from 'react-hook-form'; import { ImageBackground, StyleSheet, TouchableOpacity, View } from 'react-native'; -import PhotoUpload from '../../../../RN-UI-LIB/src/components/photoUpload/PhotoUpload'; +import PhotoUpload, { + IImageDetails, +} 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 { useAppSelector } from '../../../hooks'; +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 } from '../../../common/Constants'; +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 { @@ -35,6 +46,16 @@ interface IImageUpload { 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(''); @@ -51,6 +72,8 @@ const ImageUpload: React.FC = (props) => { ]?.questionContext?.[questionId]?.answer ); + const dispatch = useAppDispatch(); + useEffect(() => { if (dataFromRedux) { setImageId(dataFromRedux); @@ -61,8 +84,20 @@ const ImageUpload: React.FC = (props) => { return null; } - const handleChange = async (clickedImage: string | null, onChange: (...event: any[]) => void) => { - const data = `${PrefixJpegBase64Image}${clickedImage}`; + const addOriginalFileUriToDocs = (caseId: string, fileUri: string, questionKey: string) => { + if (!fileUri) { + return; + } + dispatch(addIntermediateDocument({ caseId, fileUri, questionKey })); + }; + + const handleChange = async (clickedImage: IImageDetails, onChange: (...event: any[]) => void) => { + const { base64, uri = '' } = clickedImage; + 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, { @@ -78,8 +113,14 @@ const ImageUpload: React.FC = (props) => { answer: uniqueId, type: AnswerType.image, }); - await OfflineImageDAO.addImage(data, uniqueId); + await OfflineImageDAO.addImage(base64Image, uri, uniqueId); }; + + const handleImageDelete = () => { + setImageId(''); + dispatch(deleteIntermediateDocument({ caseId, questionKey: questionId })); + }; + // TODO : add the validator back when firestore is fixed. return ( @@ -105,10 +146,10 @@ const ImageUpload: React.FC = (props) => { style={styles.image} imageStyle={styles.br8} source={{ - uri: props.offlineImages?.find((image) => image.idx === imageId)?.imageData || '', + uri: getImageUri(props.offlineImages, imageId), }} > - setImageId('')} style={styles.deleteButton}> + diff --git a/src/components/form/index.tsx b/src/components/form/index.tsx index d9cd2579..4e3ddf5e 100644 --- a/src/components/form/index.tsx +++ b/src/components/form/index.tsx @@ -41,7 +41,9 @@ import { CaptureGeolocation } from './services/geoLocation.service'; import Submit from './Submit'; import { toast } from '../../../RN-UI-LIB/src/components/toast'; import { ToastMessages } from '../../screens/allCases/constants'; +import { uploadImages } from '../../action/caseApiActions'; import { GenericFunctionArgs } from '../../common/GenericTypes'; +import { setDocumentInteractionId, setDocumentsToUpload } from '../../reducer/feedbackImagesSlice'; interface IWidget { route: { @@ -60,13 +62,27 @@ const Widget: React.FC = (props) => { const [isSubmitting, setIsSubmitting] = useState(false); const { params } = props.route; const { caseId, journey, handleCloseRouting } = params; - const caseType = useAppSelector( - (state) => - state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE - ); + const caseKey = useRef(''); + const { + caseType, + templateData, + caseData, + dataToBeValidated, + docsToBeUploaded, + intermediateDocsToBeUploaded, + } = useAppSelector((state) => { + const caseType = + state.allCases.caseDetails[caseId]?.caseType || CaseAllocationType.ADDRESS_VERIFICATION_CASE; + return { + caseType, + templateData: state.case.templateData[caseType], + caseData: state.allCases.caseDetails[caseId], + dataToBeValidated: state.case.caseForm?.[caseId]?.[journey], + docsToBeUploaded: state.feedbackImages.docsToBeUploaded, + intermediateDocsToBeUploaded: state.feedbackImages.intermediateDocsToBeUploaded, + }; + }); const name = getWidgetNameFromRoute(props.route.name, caseType); - const templateData: FormTemplateV1 = useAppSelector((state) => state.case.templateData[caseType]); - const caseData = useAppSelector((state) => state.allCases.caseDetails[caseId]); const { sections, conditionActions: widgetConditionActions, isLeaf } = templateData.widget[name]; const sectionMap = templateData.sections; const [error, setError] = useState(); @@ -82,7 +98,6 @@ const Widget: React.FC = (props) => { setIsJourneyFirstScreen(isFirst); }, [templateData, name]); - const dataToBeValidated = useAppSelector((state) => state.case.caseForm?.[caseId]?.[journey]); const { control, handleSubmit, @@ -130,7 +145,16 @@ const Widget: React.FC = (props) => { setError(data); }; - const onSuccessfulSubmit = (data: any, nextActions?: any) => { + const uploadOriginalDocs = (interactionId: string) => { + const docs = intermediateDocsToBeUploaded?.[caseId]?.documents; + if (!docs) { + return; + } + dispatch(setDocumentInteractionId({ caseKey: caseKey.current, interactionId })); + dispatch(uploadImages(caseKey.current, docs, interactionId)); + }; + + const onSuccessfulSubmit = (data: any, interactionId: string, nextActions?: any) => { setIsSubmitting(false); navigateToScreen( caseType === CaseAllocationType.COLLECTION_CASE ? 'collectionCaseDetail' : 'caseDetail', @@ -149,6 +173,9 @@ const Widget: React.FC = (props) => { nextActions, }) ); + if (interactionId) { + uploadOriginalDocs(interactionId); + } }; async function fetchLocation(): Promise { @@ -168,6 +195,16 @@ const Widget: React.FC = (props) => { }); }; + const onErrorSubmit = (updatedCaseDetails: any) => { + setIsSubmitting(false); + dispatch( + updateCaseDetail({ + caseKey: caseKey.current, + updatedCaseDetails, + }) + ); + }; + const handleSubmitJourney = async (data: any, coords: Geolocation.GeoCoordinates) => { dispatch( updateInteraction({ @@ -178,6 +215,8 @@ const Widget: React.FC = (props) => { }) ); if (caseType === CaseAllocationType.COLLECTION_CASE) { + caseKey.current = `${caseId}_${Date.now()}`; + dispatch(setDocumentsToUpload({ caseId, caseKey: caseKey.current })); const updatedCase = getUpdatedCollectionCaseDetail({ caseData, answer: data, @@ -190,17 +229,16 @@ const Widget: React.FC = (props) => { const unSyncedCase = getUnSyncedCase(updatedCase); const transformedPayload = await getTransformedCollectionCaseItem(unSyncedCase); dispatch( - syncCaseDetail(transformedPayload, updatedCase, { - onSuccessCB: onSuccessfulSubmit, - onErrorCB: () => { - setIsSubmitting(false); - }, + syncCaseDetail(transformedPayload, { + onSuccessCB: (apiCaseData, interactionId: string) => + onSuccessfulSubmit(apiCaseData, interactionId), + onErrorCB: () => onErrorSubmit(updatedCase), }) ); } else { dispatch( updateCaseDetail({ - caseId, + caseKey: caseKey.current, updatedCaseDetail: updatedCase, }) ); @@ -242,22 +280,16 @@ const Widget: React.FC = (props) => { const unSyncedCase = getUnSyncedCase(updatedCase); const transformedPayload = await getTransformedAvCase(unSyncedCase, templateId); dispatch( - syncCaseDetail( - transformedPayload, - updatedCase, - { - onSuccessCB: onSuccessfulSubmit, - onErrorCB: () => { - setIsSubmitting(false); - }, - }, - nextActions - ) + syncCaseDetail(transformedPayload, { + onSuccessCB: (apiCaseData, interactionId: string) => + onSuccessfulSubmit(apiCaseData, interactionId, nextActions), + onErrorCB: () => onErrorSubmit(updatedCase), + }) ); } else { dispatch( updateCaseDetail({ - caseId, + caseKey: caseId, updatedCaseDetail: updatedCase, }) ); diff --git a/src/components/utlis/apiHelper.ts b/src/components/utlis/apiHelper.ts index 36d13698..c4770539 100644 --- a/src/components/utlis/apiHelper.ts +++ b/src/components/utlis/apiHelper.ts @@ -49,6 +49,8 @@ export enum ApiKeys { CASES_SEND_ID = 'CASES_SEND_ID', FETCH_CASES = 'FETCH_CASES', GET_FORECLOSURE_AMOUNT = 'GET_FORECLOSURE_AMOUNT', + UPLOAD_FEEDBACK_IMAGES = 'UPLOAD_FEEDBACK_IMAGES', + ORIGINAL_IMAGES = 'ORIGINAL_IMAGES', GLOBAL_CONFIG = 'GLOBAL_CONFIG', } @@ -85,6 +87,7 @@ API_URLS[ApiKeys.CASES_SYNC_STATUS] = '/cases/agents/sync-status'; API_URLS[ApiKeys.CASES_SEND_ID] = '/cases/sync'; API_URLS[ApiKeys.FETCH_CASES] = '/cases/agents/{agentReferenceId}'; API_URLS[ApiKeys.GET_FORECLOSURE_AMOUNT] = '/{loanAccountNumber}/pre-closure-amount'; +API_URLS[ApiKeys.UPLOAD_FEEDBACK_IMAGES] = '/feedback/persist-original-images'; API_URLS[ApiKeys.GLOBAL_CONFIG] = '/global-config'; export const API_STATUS_CODE = { diff --git a/src/reducer/allCasesSlice.ts b/src/reducer/allCasesSlice.ts index d48ca14f..66e631fd 100644 --- a/src/reducer/allCasesSlice.ts +++ b/src/reducer/allCasesSlice.ts @@ -13,12 +13,7 @@ import { caseVerdict, ICaseItem, } from '../screens/allCases/interface'; -import { - CaseDetail, - CONTEXT_TASK_STATUSES, - DOCUMENT_TYPE, - IDocument, -} from '../screens/caseDetails/interface'; +import { CaseDetail, CONTEXT_TASK_STATUSES, DOCUMENT_TYPE } from '../screens/caseDetails/interface'; import { addClickstreamEvent } from '../services/clickstreamEventService'; import { getLoanAccountNumber } from '../components/utlis/commonFunctions'; import { getVisitedWidgetsNodeList } from '../components/form/services/forms.service'; @@ -26,7 +21,6 @@ import { CollectionCaseWidgetId, CommonCaseWidgetId } from '../types/template.ty import { IAvatarUri } from '../action/caseListAction'; export type ICasesMap = { [key: string]: ICaseItem }; - interface IAllCasesSlice { casesList: ICaseItem[]; casesListMap: ICasesMap; @@ -397,10 +391,8 @@ const allCasesSlice = createSlice({ } }, updateCaseDetail: (state, action) => { - const { caseId, updatedCaseDetail } = action.payload; - let caseKey: string = caseId; + const { caseKey, updatedCaseDetail } = action.payload; if (updatedCaseDetail.caseType === CaseAllocationType.COLLECTION_CASE) { - caseKey += '_' + Date.now(); updatedCaseDetail.offlineCaseKey = caseKey; } state.caseDetails[caseKey] = updatedCaseDetail; @@ -480,7 +472,9 @@ const allCasesSlice = createSlice({ ...state.caseDetails[computedKey], isSynced: true, }; - delete state.caseDetails[offlineCaseKey]; + if (offlineCaseKey) { + delete state.caseDetails[offlineCaseKey]; + } } return; } diff --git a/src/reducer/feedbackImagesSlice.ts b/src/reducer/feedbackImagesSlice.ts new file mode 100644 index 00000000..bc6e1ef1 --- /dev/null +++ b/src/reducer/feedbackImagesSlice.ts @@ -0,0 +1,93 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { isEmpty } from '../../RN-UI-LIB/src/utlis/common'; + +export interface IDocument { + fileUri?: string; +} + +interface IDocumentDetail { + interactionId: string; + documents: Record; +} + +interface IImagesSlice { + intermediateDocsToBeUploaded: Record; + docsToBeUploaded: Record; +} + +const initialState: IImagesSlice = { + intermediateDocsToBeUploaded: {}, + docsToBeUploaded: {}, +}; + +const feedbackImagesSlice = createSlice({ + name: 'feedbackImages', + initialState, + reducers: { + addIntermediateDocument: (state, action) => { + const { caseId, questionKey, fileUri } = action.payload; + const doc = { + questionKey, + fileUri, + originalImageDocumentReferenceId: '', + }; + if (state.intermediateDocsToBeUploaded?.[caseId]?.documents) { + state.intermediateDocsToBeUploaded[caseId].documents[questionKey] = doc; + } else { + state.intermediateDocsToBeUploaded[caseId] = { + interactionId: '', + documents: { + [questionKey]: doc, + }, + }; + } + }, + deleteIntermediateDocument: (state, action) => { + const { caseId, questionKey } = action.payload; + // delete respective document + if (state.intermediateDocsToBeUploaded?.[caseId]?.documents?.[questionKey]) { + delete state.intermediateDocsToBeUploaded?.[caseId]?.documents?.[questionKey]; + } + // delete the whole object if no documents remaining + if (isEmpty(state.intermediateDocsToBeUploaded?.[caseId]?.documents)) { + delete state.intermediateDocsToBeUploaded?.[caseId]; + } + }, + setDocumentsToUpload: (state, action) => { + const { caseId, caseKey } = action.payload; + if (state.intermediateDocsToBeUploaded?.[caseId]) { + state.docsToBeUploaded[caseKey] = { + ...state.intermediateDocsToBeUploaded[caseId], + }; + delete state.intermediateDocsToBeUploaded?.[caseId]; + } + }, + setDocumentInteractionId: (state, action) => { + const { caseKey, interactionId } = action.payload; + if (state.docsToBeUploaded[caseKey]) { + state.docsToBeUploaded[caseKey].interactionId = interactionId; + } + }, + removeDocumentByQuestionKey: (state, action) => { + const { caseKey, questionKey } = action.payload; + // delete respective document + if (state.docsToBeUploaded[caseKey]?.documents?.[questionKey]) { + delete state.docsToBeUploaded[caseKey]?.documents?.[questionKey]; + } + // delete the whole object if no documents remaining + if (isEmpty(state.docsToBeUploaded?.[caseKey]?.documents)) { + delete state.docsToBeUploaded?.[caseKey]; + } + }, + }, +}); + +export const { + addIntermediateDocument, + deleteIntermediateDocument, + setDocumentsToUpload, + setDocumentInteractionId, + removeDocumentByQuestionKey, +} = feedbackImagesSlice.actions; + +export default feedbackImagesSlice.reducer; diff --git a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx index 6010e43e..675938c9 100644 --- a/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx +++ b/src/screens/caseDetails/feedback/FeedbackDetailContainer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { RefreshControl, ScrollView, StyleSheet, View } from 'react-native'; import Accordion from '../../../../RN-UI-LIB/src/components/accordian/Accordian'; import NavigationHeader from '../../../../RN-UI-LIB/src/components/NavigationHeader'; @@ -67,14 +67,7 @@ const FeedbackDetailContainer: React.FC = ({ route: ro const [loading, setLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1); - useEffect(() => { - if (!isPastFeedbackOnAddress && currentPage == 1) { - setFeedbackList(feedbackListFromCache); - return; - } - - setLoading(true); - setFeedbackList([]); + const fetchFeedbacks = useCallback(() => { const getPastFeedbackApiFn = isPastFeedbackOnAddress ? getPastFeedbacksOnAddresses : getPastFeedbacks; @@ -112,6 +105,16 @@ const FeedbackDetailContainer: React.FC = ({ route: ro }); }, [currentPage]); + useEffect(() => { + if (!isPastFeedbackOnAddress && currentPage == 1) { + setFeedbackList(feedbackListFromCache); + } else { + setLoading(true); + setFeedbackList([]); + } + fetchFeedbacks(); + }, [currentPage]); + useEffect(() => { if (!isOnline) { setCurrentPage(1); @@ -167,7 +170,7 @@ const FeedbackDetailContainer: React.FC = ({ route: ro onBack={goBack} /> } + refreshControl={} style={[GenericStyles.ph16, GenericStyles.mt16]} ref={(x) => setRef(x)} > diff --git a/src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx b/src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx index b35953a6..f3bcfd16 100644 --- a/src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx +++ b/src/screens/caseDetails/feedback/FeedbackDetailImageItem.tsx @@ -28,7 +28,7 @@ const FeedbackDetailImageItem: React.FC = ({ image }) }; const questionText = getQuestionText(image); - + const originalImageUri = image.metadata?.originalDocumentSignedUri || image.inputText; return ( {questionText} @@ -54,7 +54,8 @@ const FeedbackDetailImageItem: React.FC = ({ image }) animationType="fade" > diff --git a/src/screens/caseDetails/interactionsHandler.tsx b/src/screens/caseDetails/interactionsHandler.tsx index d9e7d054..70bdcdf4 100644 --- a/src/screens/caseDetails/interactionsHandler.tsx +++ b/src/screens/caseDetails/interactionsHandler.tsx @@ -11,6 +11,8 @@ import { import { addClickstreamEvent } from '../../services/clickstreamEventService'; import { CaseAllocationType } from '../allCases/interface'; import { CaseDetail } from './interface'; +import { uploadImages } from '../../action/caseApiActions'; +import { setDocumentInteractionId } from '../../reducer/feedbackImagesSlice'; export const getUnSyncedCase = (updatedCaseDetail: CaseDetail | undefined): any => { const caseId = updatedCaseDetail?.id; @@ -21,7 +23,10 @@ export const getUnSyncedCase = (updatedCaseDetail: CaseDetail | undefined): any const interactionsHandler = () => { const dispatch = useAppDispatch(); const isOnline = useIsOnline(); - const allCasesDetails = useAppSelector((state) => state.allCases.caseDetails); + const { allCasesDetails, docsToBeUploaded } = useAppSelector((state) => ({ + allCasesDetails: state.allCases.caseDetails, + docsToBeUploaded: state.feedbackImages.docsToBeUploaded, + })); const { templateId } = useAppSelector( (state) => state.case.templateData[CaseAllocationType.ADDRESS_VERIFICATION_CASE] ); @@ -32,9 +37,19 @@ const interactionsHandler = () => { const inProgressCaseIds = useRef([]); + const handleSuccessSubmit = (caseKey: string, interactionId: string) => { + const docs = docsToBeUploaded?.[caseKey]?.documents; + if (!docs) { + return; + } + dispatch(setDocumentInteractionId({ caseKey, interactionId })); + dispatch(uploadImages(caseKey, docs, interactionId)); + }; + useEffect(() => { if (!allCasesDetails) return; let notSyncedCases: Array = []; + // [DISCUSS]: isApiCalled not getting true anywhere. _map(allCasesDetails, (el) => { if ( allCasesDetails[el]?.isSynced === false && @@ -49,19 +64,44 @@ const interactionsHandler = () => { //TODO: use batched api call for (const caseItem of notSyncedCases) { if (isOnline) { - inProgressCaseIds.current.push(caseItem.offlineCaseKey || caseItem.id); + const caseKey = caseItem.offlineCaseKey || caseItem.id; + inProgressCaseIds.current.push(caseKey); let modifiedCaseItem: any; if (caseItem?.caseType === CaseAllocationType.COLLECTION_CASE) { modifiedCaseItem = await getTransformedCollectionCaseItem(caseItem); } else { modifiedCaseItem = await getTransformedAvCase(caseItem, templateId); } - dispatch(syncCaseDetail(modifiedCaseItem)); + dispatch( + syncCaseDetail(modifiedCaseItem, { + onSuccessCB: (_, interactionId) => handleSuccessSubmit(caseKey, interactionId), + }) + ); } } })(); }, [allCasesDetails, isOnline]); + useEffect(() => { + if (!isOnline) { + return; + } + if (!docsToBeUploaded) { + return; + } + _map(docsToBeUploaded, (caseId) => { + const interactionId = docsToBeUploaded[caseId]?.interactionId; + // No interactionId means form is not submitted yet + if (!interactionId) { + return; + } + const docs = docsToBeUploaded[caseId]?.documents; + if (docs) { + dispatch(uploadImages(caseId, docs, interactionId)); + } + }); + }, [isOnline]); + return null; }; diff --git a/src/services/casePayload.transformer.ts b/src/services/casePayload.transformer.ts index cb783191..4a979dd8 100644 --- a/src/services/casePayload.transformer.ts +++ b/src/services/casePayload.transformer.ts @@ -64,7 +64,7 @@ export const extractQuestionContext = async (answer: Answer): Promise { +export const getBase64ImageFromOfflineDb = async (imageId: string) => { let imageList = await OfflineImageDAO.getImage(imageId); 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; diff --git a/src/store/store.ts b/src/store/store.ts index d2504ce3..b6e0e06b 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -27,6 +27,7 @@ import feedbackHistorySlice from '../reducer/feedbackHistorySlice'; import notificationsSlice from '../reducer/notificationsSlice'; import MetadataSlice from '../reducer/metadataSlice'; import foregroundServiceSlice from '../reducer/foregroundServiceSlice'; +import feedbackImagesSlice from '../reducer/feedbackImagesSlice'; import configSlice from '../reducer/configSlice'; const rootReducer = combineReducers({ @@ -45,6 +46,7 @@ const rootReducer = combineReducers({ notifications: notificationsSlice, metadata: MetadataSlice, foregroundService: foregroundServiceSlice, + feedbackImages: feedbackImagesSlice, config: configSlice, }); @@ -63,6 +65,7 @@ const persistConfig = { 'repayments', 'feedbackHistory', 'address', + 'feedbackImages', 'config', ], blackList: ['case', 'filters'], diff --git a/src/types/feedback.types.ts b/src/types/feedback.types.ts index 0cb64968..e92bee14 100644 --- a/src/types/feedback.types.ts +++ b/src/types/feedback.types.ts @@ -16,6 +16,11 @@ export enum OPTION_TAG { IMAGE_UPLOAD = 'IMAGE_UPLOAD', } +interface IImageMetadata { + image_created_at: string; + originalDocumentSignedUri: string; +} + export interface IAnswerView { interactionId?: number; referenceId?: string; @@ -30,6 +35,7 @@ export interface IAnswerView { inputDate?: string; inputText?: string; questionTag?: OPTION_TAG; + metadata?: IImageMetadata; } export enum FEEDBACK_TYPE { diff --git a/src/wmDB/const.ts b/src/wmDB/const.ts index acf776f0..107a9193 100644 --- a/src/wmDB/const.ts +++ b/src/wmDB/const.ts @@ -4,6 +4,6 @@ export enum TableName { CLICKSTREAM_EVENTS = 'clickstream_events', } -export const DB_VERSION = 4; +export const DB_VERSION = 5; export const DB_NAME = 'AVAPP'; diff --git a/src/wmDB/dao/OfflineImageDAO.ts b/src/wmDB/dao/OfflineImageDAO.ts index 58f69c4a..32d77426 100644 --- a/src/wmDB/dao/OfflineImageDAO.ts +++ b/src/wmDB/dao/OfflineImageDAO.ts @@ -6,12 +6,13 @@ const offlineImage = database.get(TableName.OFFLINE_IMAGES); export default { observeOfflineImage: () => offlineImage.query().observe(), - addImage: async (imageData: string, imageIdx: string) => { + addImage: async (imageData: string, originalImageUri: string, imageIdx: string) => { try { return await database.action(async () => { return await offlineImage.create((image: any) => { image.idx = imageIdx; image.imageData = imageData; + image.originalImageUri = originalImageUri; }); }); } catch (e) { diff --git a/src/wmDB/model/OfflineImage.ts b/src/wmDB/model/OfflineImage.ts index 074c7e5c..1bf3aa7a 100644 --- a/src/wmDB/model/OfflineImage.ts +++ b/src/wmDB/model/OfflineImage.ts @@ -7,6 +7,7 @@ export default class OfflineImage extends Model { @field('idx') idx!: string; @field('image_data') imageData!: string; + @field('original_image_uri') originalImageUri!: string; @readonly @date('created_at') createdAt!: any; @readonly @date('updated_at') updatedAt!: any; } diff --git a/src/wmDB/schema.ts b/src/wmDB/schema.ts index 3fa69273..1b27e451 100644 --- a/src/wmDB/schema.ts +++ b/src/wmDB/schema.ts @@ -9,6 +9,7 @@ export default appSchema({ columns: [ { name: 'idx', type: 'string' }, { name: 'image_data', type: 'string' }, + { name: 'original_image_uri', type: 'string' }, ], }), tableSchema({ diff --git a/yarn.lock b/yarn.lock index e89e004f..bf83f2c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -957,6 +957,11 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@bam.tech/react-native-image-resizer@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@bam.tech/react-native-image-resizer/-/react-native-image-resizer-3.0.5.tgz#6661ba020de156268f73bdc92fbb93ef86f88a13" + integrity sha512-u5QGUQGGVZiVCJ786k9/kd7pPRZ6eYfJCYO18myVCH8FbVI7J8b5GT2Svjj2x808DlWeqfaZOOzxPqo27XYvrQ== + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -1729,48 +1734,123 @@ redux-thunk "^2.4.2" reselect "^4.1.7" -"@sentry/browser@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.29.0.tgz#eb162b50adec33ac49ecd3dc930bdffbfda8098e" - integrity sha512-Af+dIcntaw405Wt7myDOMGDxiszfy4aBdshrEKYbGgcfHjgXBIdF3iKlNatvl6nrOm+IOVuKgSpCLOr2hiCwzw== +"@sentry-internal/tracing@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.52.0.tgz#c4e0750ad0c3949c5bb4b59cbb708b0fef274080" + integrity sha512-o1YPcRGtC9tjeFCvWRJsbgK94zpExhzfxWaldAKvi3PuWEmPeewSdO/Q5pBIY1QonvSI+Q3gysLRcVlLYHhO5A== dependencies: - "@sentry/core" "7.29.0" - "@sentry/replay" "7.29.0" - "@sentry/types" "7.29.0" - "@sentry/utils" "7.29.0" + "@sentry/core" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" tslib "^1.9.3" -"@sentry/core@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.29.0.tgz#bc4b54d56cf7652598d4430cf43ea97cc069f6fe" - integrity sha512-+e9aIp2ljtT4EJq3901z6TfEVEeqZd5cWzbKEuQzPn2UO6If9+Utd7kY2Y31eQYb4QnJgZfiIEz1HonuYY6zqQ== +"@sentry/browser@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.52.0.tgz#55d266c89ed668389ff687e5cc885c27016ea85c" + integrity sha512-Sib0T24cQCqqqAhg+nZdfI7qNYGE03jiM3RbY7yG5UoycdnJzWEwrBVSzRTgg3Uya9TRTEGJ+d9vxPIU5TL7TA== dependencies: - "@sentry/types" "7.29.0" - "@sentry/utils" "7.29.0" + "@sentry-internal/tracing" "7.52.0" + "@sentry/core" "7.52.0" + "@sentry/replay" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" tslib "^1.9.3" -"@sentry/replay@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.29.0.tgz#75d5bb9df39e0a31994be245032c9998af62a304" - integrity sha512-Gw7HgviJQu6pX5RFQGVY38Av4qFn9otrZdwSSl/QK5hIyg6yhlh5h7U0ydZkrYYGiW6Z6SYYRpEWCJc/Wbh+ZQ== +"@sentry/cli@2.17.5": + version "2.17.5" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.17.5.tgz#d41e24893a843bcd41e14274044a7ddea9332824" + integrity sha512-0tXjLDpaKB46851EMJ6NbP0o9/gdEaDSLAyjEtXxlVO6+RyhUj6x6jDwn0vis8n/7q0AvbIjAcJrot+TbZP+WQ== dependencies: - "@sentry/core" "7.29.0" - "@sentry/types" "7.29.0" - "@sentry/utils" "7.29.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" -"@sentry/types@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.29.0.tgz#ed829b6014ee19049035fec6af2b4fea44ff28b8" - integrity sha512-DmoEpoqHPty3VxqubS/5gxarwebHRlcBd/yuno+PS3xy++/i9YPjOWLZhU2jYs1cW68M9R6CcCOiC9f2ckJjdw== - -"@sentry/utils@7.29.0": - version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.29.0.tgz#cbf8f87dd851b0fdc7870db9c68014c321c3bab8" - integrity sha512-ICcBwTiBGK8NQA8H2BJo0JcMN6yCeKLqNKNMVampRgS6wSfSk1edvcTdhRkW3bSktIGrIPZrKskBHyMwDGF2XQ== +"@sentry/core@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.52.0.tgz#6c820ca48fe2f06bfd6b290044c96de2375f2ad4" + integrity sha512-BWdG6vCMeUeMhF4ILpxXTmw70JJvT1MGJcnv09oSupWHTmqy6I19YP6YcEyFuBL4jXPN51eCl7luIdLGJrPbOg== dependencies: - "@sentry/types" "7.29.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" tslib "^1.9.3" +"@sentry/hub@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.52.0.tgz#ffc087d58c745d57108862faa0f701b15503dcc2" + integrity sha512-w3d8Pmp3Fx2zbbjz6hAeIbsFEkLyrUs9YTGG2y8oCoTlAtGK+AjdG+Z0H/clAZONflD/je2EmFHCI0EuXE9tEw== + dependencies: + "@sentry/core" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" + tslib "^1.9.3" + +"@sentry/integrations@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.52.0.tgz#632aa5e54bdfdab910a24057c2072634a2670409" + integrity sha512-tqxYzgc71XdFD8MTCsVMCPef08lPY9jULE5Zi7TzjyV2AItDRJPkixG0qjwjOGwCtN/6KKz0lGPGYU8ZDxvsbg== + dependencies: + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" + localforage "^1.8.1" + tslib "^1.9.3" + +"@sentry/react-native@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-5.5.0.tgz#b1283f68465b1772ad6059ebba149673cef33f2d" + integrity sha512-xrES+OAIu3HFhoQSuJjd16Hh02/mByuNoKUjF7e4WDGIiTew3aqlqeLjU7x4npmg5Vbt+ND5jR12u/NmdfArwg== + dependencies: + "@sentry/browser" "7.52.0" + "@sentry/cli" "2.17.5" + "@sentry/core" "7.52.0" + "@sentry/hub" "7.52.0" + "@sentry/integrations" "7.52.0" + "@sentry/react" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" + +"@sentry/react@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.52.0.tgz#d12d270ec82dea0474e69deb9112181affe7c524" + integrity sha512-VQxquyFFlvB81k7UER7tTJxjzbczNI2jqsw6nN1TVDrAIDt8/hT2x7m/M0FlWc88roBKuaMmbvzfNGWaL9abyQ== + dependencies: + "@sentry/browser" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" + hoist-non-react-statics "^3.3.2" + tslib "^1.9.3" + +"@sentry/replay@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.52.0.tgz#4d78e88282d2c1044ea4b648a68d1b22173e810d" + integrity sha512-RRPALjDST2s7MHiMcUJ7Wo4WW7EWfUDYSG0LuhMT8DNc+ZsxQoFsLYX/yz8b3f0IUSr7xKBXP+aPeIy3jDAS2g== + dependencies: + "@sentry/core" "7.52.0" + "@sentry/types" "7.52.0" + "@sentry/utils" "7.52.0" + +"@sentry/types@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.52.0.tgz#b7d5372f17355e3991cbe818ad567f3fe277cc6b" + integrity sha512-XnEWpS6P6UdP1FqbmeqhI96Iowqd2jM5R7zJ97txTdAd5NmdHHH0pODTR9NiQViA1WlsXDut7ZLxgPzC9vIcMA== + +"@sentry/utils@7.52.0": + version "7.52.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.52.0.tgz#cacc36d905036ba7084c14965e964fc44239d7f0" + integrity sha512-X1NHYuqW0qpZfP731YcVe+cn36wJdAeBHPYPIkXCl4o4GePCJfH/CM/+9V9cZykNjyLrs2Xy/TavSAHNCj8j7w== + dependencies: + "@sentry/types" "7.52.0" + tslib "^1.9.3" + +"@shopify/flash-list@1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.4.3.tgz#b7a4fe03d64f3c5ce9646859b49b9d95307f203d" + integrity sha512-jtIReAbwWzYBV0dQ6Io9wBX+pD0C4qQFMrb5/fkEvX8PYDgBl5KRYvpfr9WLLj8CV2Jsn1X0mYOsB+ysWrI/8g== + dependencies: + recyclerlistview "4.2.0" + tslib "2.4.0" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -4858,6 +4938,11 @@ image-size@^0.6.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.6.3.tgz#e7e5c65bb534bd7cdcedd6cb5166272a85f75fb2" integrity sha512-47xSUiQioGaB96nqtp5/q55m0aBQSQdyIloMOc/x+QVTDZLNmXE892IIDrJ0hM1A5vcNUDD5tDffkSP5lCaIIA== +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ== + immer@^9.0.16: version "9.0.16" resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.16.tgz#8e7caab80118c2b54b37ad43e05758cdefad0198" @@ -5977,6 +6062,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lie@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" + integrity sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw== + dependencies: + immediate "~3.0.5" + lilconfig@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -6020,6 +6112,13 @@ listr2@^5.0.7: through "^2.3.8" wrap-ansi "^7.0.0" +localforage@^1.8.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" + integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== + dependencies: + lie "3.1.1" + locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -6062,7 +6161,7 @@ lodash.compact@^3.0.1: resolved "https://registry.yarnpkg.com/lodash.compact/-/lodash.compact-3.0.1.tgz#540ce3837745975807471e16b4a2ba21e7256ca5" integrity sha512-2ozeiPi+5eBXW1CLtzjk8XQFhQOEMwwfxblqeq6EGyTxZJ1bPATqilY0e6g2SLQpP4KuMeuioBhEnWz5Pr7ICQ== -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== @@ -6825,6 +6924,13 @@ node-fetch@^2.2.0, node-fetch@^2.6.0: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.7: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -7387,6 +7493,11 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise-polyfill@^8.1.3: version "8.3.0" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63" @@ -7407,7 +7518,7 @@ prompts@^2.0.1, prompts@^2.4.0: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2, prop-types@^15.8.0, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.0, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -7872,6 +7983,15 @@ recursive-fs@^2.1.0: resolved "https://registry.yarnpkg.com/recursive-fs/-/recursive-fs-2.1.0.tgz#1e20cf7836b292ed81208c4817550a58ad0e15ff" integrity sha512-oed3YruYsD52Mi16s/07eYblQOLi5dTtxpIJNdfCEJ7S5v8dDgVcycar0pRWf4IBuPMIkoctC8RTqGJzIKMNAQ== +recyclerlistview@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.0.tgz#a140149aaa470c9787a1426452651934240d69ef" + integrity sha512-uuBCi0c+ggqHKwrzPX4Z/mJOzsBbjZEAwGGmlwpD/sD7raXixdAbdJ6BTcAmuWG50Cg4ru9p12M94Njwhr/27A== + dependencies: + lodash.debounce "4.0.8" + prop-types "15.8.1" + ts-object-utils "0.0.5" + redux-persist@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" @@ -9004,6 +9124,11 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +ts-object-utils@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" + integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== + tsconfig-paths@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" @@ -9014,6 +9139,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"