Image Fallback API integration for non-signed urls | Himanshu (#52)
* non-signed images handle * Fix review comment * Saving & Fetching customer images from wm db * Review comment resolve * Fix component break * Reuse component CaseItemAvatar * Fix review comment * Minor fix
This commit is contained in:
committed by
GitHub Enterprise
parent
b1e304e4ad
commit
635ca7b675
@@ -41,7 +41,8 @@
|
||||
"react-native-toast-message": "2.1.5",
|
||||
"react-redux": "8.0.5",
|
||||
"redux": "4.2.0",
|
||||
"redux-persist": "6.0.0"
|
||||
"redux-persist": "6.0.0",
|
||||
"rn-fetch-blob": "0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.12.9",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../reducer/allCasesSlice';
|
||||
import { ICaseItem } from '../screens/allCases/interface';
|
||||
import { AppDispatch } from '../store/store';
|
||||
import { logError } from '../components/utlis/errorUtils';
|
||||
|
||||
export const getAllCases =
|
||||
() => (dispatch: AppDispatch) => {
|
||||
@@ -131,3 +132,19 @@ export const getFilters = () => (dispatch: AppDispatch) => {
|
||||
dispatch(setFilters(response.data));
|
||||
});
|
||||
};
|
||||
|
||||
export const getSignedURLs = (urlList: string[]) => (dispatch: AppDispatch) => {
|
||||
const url = getApiUrl(ApiKeys.IMAGE_SIGNED_URLS);
|
||||
dispatch(setLoading(true));
|
||||
return axiosInstance.post(url, urlList).then(response => {
|
||||
if(response?.data) {
|
||||
return response.data;
|
||||
}
|
||||
throw response;
|
||||
}).catch((err) => {
|
||||
logError(err);
|
||||
})
|
||||
.finally(() => {
|
||||
dispatch(setLoading(false));
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CaseStatuses } from "../screens/allCases/interface";
|
||||
|
||||
export enum CONDITIONAL_OPERATORS {
|
||||
EQUALS = 'EQUALS',
|
||||
LESS_THAN_EQUAL_TO = 'LESS_THAN_EQUAL_TO',
|
||||
@@ -24,3 +26,16 @@ export enum FirestoreUpdateTypes {
|
||||
MODIFIED = 'modified',
|
||||
REMOVED = 'removed',
|
||||
}
|
||||
|
||||
export enum MimeType {
|
||||
'image/jpeg' = 'image/jpeg',
|
||||
'image/png' = 'image/png'
|
||||
}
|
||||
|
||||
export const ClosedCaseStatusList = [CaseStatuses.CLOSED, CaseStatuses.FORCE_CLOSE, CaseStatuses.EXPIRED];
|
||||
|
||||
export const getPrefixBase64Image = (contentType: MimeType) => {
|
||||
return `data:${contentType};base64,}`;
|
||||
}
|
||||
|
||||
export const PrefixJpegBase64Image = getPrefixBase64Image(MimeType["image/jpeg"]);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { AnswerType } from '../interface';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import OfflineImageDAO from '../../../wmDB/dao/OfflineImageDAO';
|
||||
import { PrefixJpegBase64Image } from '../../../common/Constants';
|
||||
|
||||
interface IOfflineImage {
|
||||
idx: string;
|
||||
@@ -63,7 +64,7 @@ const ImageUpload: React.FC<IImageUpload> = props => {
|
||||
clickedImage: string | null,
|
||||
onChange: (...event: any[]) => void,
|
||||
) => {
|
||||
const data = `data:image/jpeg;base64,${clickedImage}`;
|
||||
const data = `${PrefixJpegBase64Image}${clickedImage}`;
|
||||
var uniqueId = 'id' + (new Date()).getTime();
|
||||
setImageId(uniqueId);
|
||||
onChange({
|
||||
|
||||
@@ -23,6 +23,7 @@ export enum ApiKeys {
|
||||
LOGOUT,
|
||||
FEEDBACK,
|
||||
FILTERS,
|
||||
IMAGE_SIGNED_URLS
|
||||
}
|
||||
|
||||
const API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
|
||||
@@ -34,6 +35,7 @@ API_URLS[ApiKeys.PINNED_CASES] = '/cases/pin';
|
||||
API_URLS[ApiKeys.LOGOUT] = '/auth/logout';
|
||||
API_URLS[ApiKeys.FEEDBACK] = '/cases/feedback';
|
||||
API_URLS[ApiKeys.FILTERS] = '/cases/filters';
|
||||
API_URLS[ApiKeys.IMAGE_SIGNED_URLS] = '/cases/get-signed-urls';
|
||||
|
||||
const MOCK_API_URLS: Record<ApiKeys, string> = {} as Record<ApiKeys, string>;
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import RNFetchBlob from "rn-fetch-blob";
|
||||
import { getPrefixBase64Image, MimeType } from "../../common/Constants";
|
||||
|
||||
import { Address } from "../../screens/caseDetails/interface";
|
||||
|
||||
const fs = RNFetchBlob.fs;
|
||||
|
||||
const RespHeaderContentTypeKey = 'Content-Type'
|
||||
|
||||
export const decideLoadingState = (textData: string): boolean => {
|
||||
if (!textData) {
|
||||
return true;
|
||||
@@ -17,3 +24,24 @@ export const getCommunicationAddress = (address: Address) => {
|
||||
const { houseNumber, lineOne, lineTwo, locality, street, city, state, pinCode } = address;
|
||||
return [houseNumber, lineOne, lineTwo, locality, street, city, state, pinCode].filter(element => element).join(', ');
|
||||
}
|
||||
|
||||
export const getBase64FromUrl = async (imagePath: string) => {
|
||||
let contentType: MimeType;
|
||||
return RNFetchBlob.config({
|
||||
fileCache: true
|
||||
}).fetch("GET", imagePath)
|
||||
.then(resp => {
|
||||
if (resp) {
|
||||
imagePath = resp.path();
|
||||
contentType = resp.respInfo?.headers?.[RespHeaderContentTypeKey];
|
||||
return resp.readFile("base64");
|
||||
}
|
||||
})
|
||||
.then(base64 => {
|
||||
fs.unlink(imagePath);
|
||||
if (contentType) {
|
||||
return `${getPrefixBase64Image(contentType)}${base64}`;
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
55
src/components/utlis/customerDbHelper.ts
Normal file
55
src/components/utlis/customerDbHelper.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { ClosedCaseStatusList } from "../../common/Constants";
|
||||
import { GenericType } from "../../common/GenericTypes";
|
||||
import { CustomerImageDAO } from "../../wmDB";
|
||||
import { getBase64FromUrl } from "./commonFunctions";
|
||||
|
||||
const isCaseIdRecordExistOnCustomerDb = async (caseId: string) => {
|
||||
let imageList = await CustomerImageDAO.getImageByCase(caseId);
|
||||
return !!imageList?.length;
|
||||
}
|
||||
|
||||
export const getImage64FromCaseId = async (caseId: string) => {
|
||||
let imageList = await CustomerImageDAO.getImageByCase(caseId);
|
||||
return imageList?.[0]?.image64;
|
||||
}
|
||||
|
||||
export const updateRecordsOnCustomerDbFromCaseList = (caseList: GenericType[]) => {
|
||||
if (!caseList?.length) return;
|
||||
|
||||
const mappedCaseList = caseList
|
||||
.map((caseItem: GenericType) => (
|
||||
{
|
||||
imageURL: caseItem?.customerInfo?.imageURL,
|
||||
caseId: caseItem?.id,
|
||||
caseStatus: caseItem?.caseStatus
|
||||
}
|
||||
));
|
||||
|
||||
const filteredMappedCaseList = [];
|
||||
const closedCaseIdList = [];
|
||||
|
||||
(async () => {
|
||||
for(const caseItem of mappedCaseList) {
|
||||
if (caseItem.caseId && caseItem.imageURL) {
|
||||
const isCaseIdRecordExist = await isCaseIdRecordExistOnCustomerDb(caseItem.caseId);
|
||||
|
||||
if(isCaseIdRecordExist && ClosedCaseStatusList.includes(caseItem.caseStatus)) {
|
||||
closedCaseIdList.push(caseItem.caseId);
|
||||
}
|
||||
|
||||
if (!isCaseIdRecordExist && !ClosedCaseStatusList.includes(caseItem.caseStatus)) {
|
||||
const image64 = await getBase64FromUrl(caseItem.imageURL);
|
||||
if(image64) {
|
||||
filteredMappedCaseList.push({...caseItem, image64});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (filteredMappedCaseList?.length) {
|
||||
await CustomerImageDAO.addBulkCustomerImage(filteredMappedCaseList);
|
||||
}
|
||||
if (closedCaseIdList?.length) {
|
||||
await CustomerImageDAO.deleteImages(closedCaseIdList);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '../common/Constants';
|
||||
import { CaseDetail } from '../screens/caseDetails/interface';
|
||||
import { CaseUpdates } from '../hooks/useFirestoreUpdates';
|
||||
import { updateRecordsOnCustomerDbFromCaseList } from '../components/utlis/customerDbHelper';
|
||||
import { _map } from '../../RN-UI-LIB/src/utlis/common';
|
||||
|
||||
export type ICasesMap = { [key: string]: ICaseItem };
|
||||
@@ -119,6 +120,8 @@ const allCasesSlice = createSlice({
|
||||
},
|
||||
setCasesDetailsData: (state, action) => {
|
||||
const { details } = action.payload;
|
||||
updateRecordsOnCustomerDbFromCaseList(details);
|
||||
|
||||
if (details?.length) {
|
||||
details.forEach((caseDetail: CaseDetail) => {
|
||||
const { id } = caseDetail;
|
||||
|
||||
@@ -1,40 +1,83 @@
|
||||
import React from 'react';
|
||||
import {ICaseItem} from './interface';
|
||||
import RoundCheckIcon from '../../icons/RoundCheckIcon';
|
||||
import {useAppSelector} from '../../hooks';
|
||||
import {useAppDispatch, useAppSelector} from '../../hooks';
|
||||
import {CaseDetail} from '../caseDetails/interface';
|
||||
import Avatar from '../../../RN-UI-LIB/src/components/Avatar';
|
||||
import UnsyncedIcon from '../../../RN-UI-LIB/src/Icons/UnsyncedIcon';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { getSignedURLs } from '../../action/dataActions';
|
||||
import { getImage64FromCaseId } from '../../components/utlis/customerDbHelper';
|
||||
|
||||
interface ICaseItemAvatar {
|
||||
caseSelected: boolean;
|
||||
caseSelected?: boolean;
|
||||
caseData: ICaseItem;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const MAX_API_CALL = 3;
|
||||
|
||||
const CaseItemAvatar: React.FC<ICaseItemAvatar> = ({
|
||||
caseSelected,
|
||||
caseSelected = false,
|
||||
caseData,
|
||||
size = 32
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const {caseReferenceId} = caseData;
|
||||
const caseDetails: CaseDetail = useAppSelector(
|
||||
state => state.allCases.caseDetails?.[caseReferenceId],
|
||||
);
|
||||
if (caseSelected) {
|
||||
return <RoundCheckIcon />;
|
||||
}
|
||||
|
||||
const [imageUrl, setImageUrl] = React.useState<string>('');
|
||||
const [apiErrorCount, setApiErrorCount] = React.useState(0);
|
||||
|
||||
const isSynced = caseDetails?.isSynced;
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
if (caseDetails?.customerInfo?.imageURL) {
|
||||
(async () => {
|
||||
let image64 = await getImage64FromCaseId(caseDetails?.id);
|
||||
if (image64) { // if image exist in WM db, then setting the base64
|
||||
setImageUrl(image64);
|
||||
} else {
|
||||
setImageUrl(caseDetails.customerInfo.imageURL)
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [caseDetails?.customerInfo?.imageURL]);
|
||||
|
||||
const onError = async () => {
|
||||
if (apiErrorCount < MAX_API_CALL) {
|
||||
dispatch(getSignedURLs([imageUrl])).then((resp) => {
|
||||
if (resp?.[imageUrl]) {
|
||||
setApiErrorCount(apiErrorCount => apiErrorCount+1);
|
||||
setImageUrl(resp[imageUrl])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Avatar
|
||||
name={caseDetails?.customerInfo?.name || caseDetails?.customerInfo?.customerName }
|
||||
dataURI={caseDetails.customerInfo.imageURL}
|
||||
/>
|
||||
{!isSynced ? (
|
||||
<View style={styles.unsyncedIcon}>
|
||||
<UnsyncedIcon />
|
||||
</View>
|
||||
) : null}
|
||||
{
|
||||
!caseSelected ? (
|
||||
<View>
|
||||
<Avatar
|
||||
size={size}
|
||||
name={caseDetails?.customerInfo?.name || caseDetails?.customerInfo?.customerName }
|
||||
dataURI={imageUrl}
|
||||
onErrorFallback={onError}
|
||||
/>
|
||||
{!isSynced ? (
|
||||
<View style={styles.unsyncedIcon}>
|
||||
<UnsyncedIcon />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : <RoundCheckIcon />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,8 @@ import CalenderIcon from '../../assets/icons/CalenderIcon';
|
||||
import DocumentIcon from '../../assets/icons/DocumentIcon';
|
||||
import IconLabel from '../../common/IconLabel';
|
||||
import { decideLoadingState } from '../../components/utlis/commonFunctions';
|
||||
import CaseItemAvatar from '../allCases/CaseItemAvatar';
|
||||
import { ICaseItem } from '../allCases/interface';
|
||||
import {
|
||||
CaseDetail,
|
||||
LoanAccountStatusUIMapping,
|
||||
@@ -64,16 +66,10 @@ const UserDetailsSection: React.FC<IUserDetailsSection> = props => {
|
||||
/>
|
||||
}>
|
||||
<TouchableOpacity onPress={handleOpenClose}>
|
||||
<Avatar
|
||||
<CaseItemAvatar
|
||||
size={64}
|
||||
name={customerInfo?.customerName || customerInfo?.name || ''}
|
||||
dataURI={customerInfo?.imageURL}
|
||||
caseData={{...caseDetail, caseReferenceId: caseDetail?.id} as ICaseItem}
|
||||
/>
|
||||
{!isSynced ? (
|
||||
<View style={styles.unsyncedIcon}>
|
||||
<UnsyncedIcon />
|
||||
</View>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
</SuspenseLoader>
|
||||
<View style={[styles.infoContainer]}>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export enum TableName {
|
||||
OFFLINE_IMAGES = 'offline_image',
|
||||
CUSTOMER_IMAGE = 'customer_image'
|
||||
};
|
||||
|
||||
export const DB_VERSION = 1;
|
||||
export const DB_VERSION = 2;
|
||||
|
||||
export const DB_NAME = 'AVAPP';
|
||||
|
||||
94
src/wmDB/dao/CustomerImageDAO.ts
Normal file
94
src/wmDB/dao/CustomerImageDAO.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { database } from '../db';
|
||||
import { Q } from '@nozbe/watermelondb'
|
||||
import { TableName } from '../const';
|
||||
import { logError } from '../../components/utlis/errorUtils';
|
||||
|
||||
const customerImage = database.get(TableName.CUSTOMER_IMAGE);
|
||||
|
||||
interface ICustomerOfflineImage {
|
||||
caseId: string;
|
||||
imageURL: string;
|
||||
image64: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
observeOfflineImage: () => customerImage.query().observe(),
|
||||
addBulkCustomerImage: async (customerImageList: ICustomerOfflineImage[]) => {
|
||||
function prepareInsertion(customerImageList: ICustomerOfflineImage[]) {
|
||||
return customerImageList.map(customerImageItem => {
|
||||
try {
|
||||
return customerImage.prepareCreate((imageRow: any) => {
|
||||
imageRow.caseId = customerImageItem.caseId;
|
||||
imageRow.imageURL = customerImageItem.imageURL;
|
||||
imageRow.image64 = customerImageItem.image64;
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
}
|
||||
});
|
||||
}
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
const allRecords = prepareInsertion(customerImageList);
|
||||
await database.batch(...allRecords);
|
||||
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
}
|
||||
},
|
||||
addCustomerImage: async (customerImageItem: ICustomerOfflineImage) => {
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
return await customerImage.create((imageRow: any) => {
|
||||
imageRow.caseId = customerImageItem.caseId;
|
||||
imageRow.imageURL = customerImageItem.imageURL;
|
||||
imageRow.image64 = customerImageItem.image64;
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
}
|
||||
},
|
||||
getImageByURL: async (imageURL: string) => {
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
return await customerImage.query(Q.where('image_url', imageURL), Q.take(1)).fetch()
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
return;
|
||||
}
|
||||
},
|
||||
getImageByCase: async (caseId: string) => {
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
return await customerImage.query(Q.where('case_id', caseId), Q.take(1)).fetch()
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
return;
|
||||
}
|
||||
},
|
||||
deleteImages: async (caseIds: Array<string> | string) => {
|
||||
let tCaseIds = Array.isArray(caseIds) ? caseIds : [caseIds];
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
await customerImage.query(Q.where('case_id', Q.oneOf(tCaseIds))).destroyAllPermanently()
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
return;
|
||||
}
|
||||
},
|
||||
deleteAll: async () => {
|
||||
try {
|
||||
return await database.action(async () => {
|
||||
await customerImage.query().destroyAllPermanently();
|
||||
});
|
||||
} catch (e) {
|
||||
logError(e as Error, "WM DB")
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Database } from '@nozbe/watermelondb';
|
||||
import SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';
|
||||
import { DB_NAME } from './const';
|
||||
import CustomerImage from './model/CustomerImage';
|
||||
import OfflineImage from './model/OfflineImage';
|
||||
|
||||
import schema from './schema';
|
||||
@@ -12,7 +13,7 @@ const adapter = new SQLiteAdapter({
|
||||
|
||||
const database = new Database({
|
||||
adapter,
|
||||
modelClasses: [OfflineImage]
|
||||
modelClasses: [OfflineImage, CustomerImage]
|
||||
});
|
||||
|
||||
export { database };
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PostDAO from './dao/OfflineImageDAO';
|
||||
import CustomerImageDAO from './dao/CustomerImageDAO';
|
||||
|
||||
export { database } from './db';
|
||||
export { PostDAO };
|
||||
export { PostDAO, CustomerImageDAO };
|
||||
|
||||
17
src/wmDB/model/CustomerImage.ts
Normal file
17
src/wmDB/model/CustomerImage.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Model } from '@nozbe/watermelondb';
|
||||
import {
|
||||
field,
|
||||
readonly,
|
||||
date,
|
||||
} from '@nozbe/watermelondb/decorators';
|
||||
import { TableName } from '../const';
|
||||
|
||||
export default class CustomerImage extends Model {
|
||||
static table = TableName.CUSTOMER_IMAGE;
|
||||
|
||||
@field('case_id') caseId!: string;
|
||||
@field('image_url') imageURL!: string;
|
||||
@field('image_64') image64!: string;
|
||||
@readonly @date('created_at') createdAt!: any;
|
||||
@readonly @date('updated_at') updatedAt!: any;
|
||||
}
|
||||
@@ -10,6 +10,14 @@ export default appSchema({
|
||||
{ name: 'idx', type: 'string' },
|
||||
{ name: 'image_data', type: 'string' },
|
||||
]
|
||||
}),
|
||||
tableSchema({
|
||||
name: TableName.CUSTOMER_IMAGE,
|
||||
columns: [
|
||||
{ name: 'case_id', type: 'string' },
|
||||
{ name: 'image_url', type: 'string' },
|
||||
{ name: 'image_64', type: 'string' },
|
||||
]
|
||||
})
|
||||
],
|
||||
});
|
||||
25
yarn.lock
25
yarn.lock
@@ -2531,6 +2531,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
base-64@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
||||
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
||||
|
||||
base64-js@^1.1.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
@@ -4137,6 +4142,18 @@ glob-parent@^6.0.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.3"
|
||||
|
||||
glob@7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a"
|
||||
integrity sha512-f8c0rE8JiCxpa52kWPAOa3ZaYEnzofDzCQLCn3Vdk0Z5OVLq3BsRFJI4S4ykpeVW6QMGBUkMeUpoEgWnMTnw5Q==
|
||||
dependencies:
|
||||
fs.realpath "^1.0.0"
|
||||
inflight "^1.0.4"
|
||||
inherits "2"
|
||||
minimatch "^3.0.2"
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
@@ -7060,6 +7077,14 @@ rimraf@~2.6.2:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rn-fetch-blob@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/rn-fetch-blob/-/rn-fetch-blob-0.12.0.tgz#ec610d2f9b3f1065556b58ab9c106eeb256f3cba"
|
||||
integrity sha512-+QnR7AsJ14zqpVVUbzbtAjq0iI8c9tCg49tIoKO2ezjzRunN7YL6zFSFSWZm6d+mE/l9r+OeDM3jmb2tBb2WbA==
|
||||
dependencies:
|
||||
base-64 "0.1.0"
|
||||
glob "7.0.6"
|
||||
|
||||
rsvp@^4.8.4:
|
||||
version "4.8.5"
|
||||
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
|
||||
|
||||
Reference in New Issue
Block a user