First prototype of image upload working. IO-397 IO-398
This commit is contained in:
@@ -47,7 +47,6 @@ export function ScreenCamera({ cameraJobId, cameraJob, addPhoto }) {
|
||||
};
|
||||
|
||||
const handleShortCapture = async () => {
|
||||
console.log("Taking the picture!");
|
||||
if (cameraRef.current) {
|
||||
const options = {
|
||||
//quality: 0.5,
|
||||
@@ -109,7 +108,7 @@ export function ScreenCamera({ cameraJobId, cameraJob, addPhoto }) {
|
||||
return <Text>No access to camera</Text>;
|
||||
}
|
||||
|
||||
const { hasCameraPermission, flashMode, cameraType, capturing } = state;
|
||||
const { flashMode, cameraType, capturing } = state;
|
||||
|
||||
return (
|
||||
<View style={{ display: "flex", flex: 1 }}>
|
||||
|
||||
@@ -3,37 +3,44 @@ import React from "react";
|
||||
import { FlatList, SafeAreaView, Text } from "react-native";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { removeAllPhotos } from "../../redux/photos/photos.actions";
|
||||
import {
|
||||
removeAllPhotos,
|
||||
uploadAllPhotos,
|
||||
} from "../../redux/photos/photos.actions";
|
||||
import { selectPhotos } from "../../redux/photos/photos.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
photos: selectPhotos,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
removeAllPhotos: () => dispatch(removeAllPhotos()),
|
||||
uploadAllphotos: () => dispatch(uploadAllPhotos()),
|
||||
});
|
||||
|
||||
export function ScreenMediaCache({ photos, removeAllPhotos }) {
|
||||
export function ScreenMediaCache({ photos, removeAllPhotos, uploadAllphotos }) {
|
||||
return (
|
||||
<SafeAreaView style={{ display: "flex", flex: 1 }}>
|
||||
<Text>This is the media cache screen.</Text>
|
||||
<Button block onPress={() => removeAllPhotos()}>
|
||||
<NBText>Delete all</NBText>
|
||||
</Button>
|
||||
<Button block onPress={() => uploadAllphotos()}>
|
||||
<NBText>Upload all</NBText>
|
||||
</Button>
|
||||
<Text>{photos.length}</Text>
|
||||
|
||||
<FlatList
|
||||
style={{ flex: 1, backgroundColor: "tomato" }}
|
||||
style={{ flex: 1 }}
|
||||
data={photos}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={(object) => (
|
||||
<View>
|
||||
<Text>{object.item.uri}</Text>
|
||||
<Thumbnail square large source={{ uri: object.item.uri }} />
|
||||
{!object.item.video && (
|
||||
<Thumbnail square large source={{ uri: object.item.uri }} />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
//ItemSeparatorComponent={FlatListItemSeparator}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
39
env.js
Normal file
39
env.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import Constants from "expo-constants";
|
||||
|
||||
export const prodUrl = "https://someapp.herokuapp.com";
|
||||
|
||||
const ENV = {
|
||||
dev: {
|
||||
REACT_APP_CLOUDINARY_ENDPOINT:
|
||||
"https://api.cloudinary.com/v1_1/bodyshop/image",
|
||||
REACT_APP_CLOUDINARY_IMAGE_ENDPOINT:
|
||||
"https://res.cloudinary.com/bodyshop/image/upload",
|
||||
REACT_APP_CLOUDINARY_API_KEY: "473322739956866",
|
||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS: "h_200,w_200,c_thumb",
|
||||
},
|
||||
staging: {
|
||||
REACT_APP_CLOUDINARY_ENDPOINT:
|
||||
"https://api.cloudinary.com/v1_1/bodyshop/image",
|
||||
REACT_APP_CLOUDINARY_IMAGE_ENDPOINT:
|
||||
"https://res.cloudinary.com/bodyshop/image/upload",
|
||||
REACT_APP_CLOUDINARY_API_KEY: "473322739956866",
|
||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS: "h_200,w_200,c_thumb",
|
||||
},
|
||||
prod: {
|
||||
REACT_APP_CLOUDINARY_ENDPOINT:
|
||||
"https://api.cloudinary.com/v1_1/bodyshop/image",
|
||||
REACT_APP_CLOUDINARY_IMAGE_ENDPOINT:
|
||||
"https://res.cloudinary.com/bodyshop/image/upload",
|
||||
REACT_APP_CLOUDINARY_API_KEY: "473322739956866",
|
||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS: "h_200,w_200,c_thumb",
|
||||
},
|
||||
};
|
||||
|
||||
function getEnvVars(env = "") {
|
||||
if (env === null || env === undefined || env === "") return ENV.dev;
|
||||
if (env.indexOf("dev") !== -1) return ENV.dev;
|
||||
if (env.indexOf("staging") !== -1) return ENV.staging;
|
||||
if (env.indexOf("prod") !== -1) return ENV.prod;
|
||||
}
|
||||
|
||||
export default getEnvVars(Constants.manifest.releaseChannel);
|
||||
@@ -3,6 +3,8 @@ import gql from "graphql-tag";
|
||||
export const QUERY_BODYSHOP = gql`
|
||||
query QUERY_BODYSHOP {
|
||||
bodyshops(where: { associations: { active: { _eq: true } } }) {
|
||||
id
|
||||
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
shopname
|
||||
|
||||
46
graphql/documents.queries.js
Normal file
46
graphql/documents.queries.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import gql from "graphql-tag";
|
||||
|
||||
export const GET_DOCUMENTS_BY_JOB = gql`
|
||||
query GET_DOCUMENTS_BY_JOB($jobId: uuid!) {
|
||||
documents(
|
||||
where: { jobid: { _eq: $jobId } }
|
||||
order_by: { updated_at: desc }
|
||||
) {
|
||||
id
|
||||
name
|
||||
key
|
||||
type
|
||||
bill {
|
||||
id
|
||||
invoice_number
|
||||
date
|
||||
vendor {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSERT_NEW_DOCUMENT = gql`
|
||||
mutation INSERT_NEW_DOCUMENT($docInput: [documents_insert_input!]!) {
|
||||
insert_documents(objects: $docInput) {
|
||||
returning {
|
||||
id
|
||||
name
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_DOCUMENT = gql`
|
||||
mutation DELETE_DOCUMENT($id: uuid) {
|
||||
delete_documents(where: { id: { _eq: $id } }) {
|
||||
returning {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -2848,6 +2848,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
|
||||
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"babel-plugin-dynamic-import-node": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
|
||||
@@ -4425,6 +4433,11 @@
|
||||
"@firebase/util": "0.2.40"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
|
||||
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
|
||||
},
|
||||
"fontfaceobserver": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.1.0.tgz",
|
||||
@@ -7945,6 +7958,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-native-image-base64": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-base64/-/react-native-image-base64-0.1.4.tgz",
|
||||
"integrity": "sha512-WLdzwHdXFRLS9VStG1CG46+t+fcInjVWxKd1+AATElBhaAS3zwDHz7mYIZS1OX4VMuNClwl5G8dowuqUJ9aMGQ=="
|
||||
},
|
||||
"react-native-indicators": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-indicators/-/react-native-indicators-0.17.0.tgz",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@react-navigation/drawer": "^5.10.7",
|
||||
"@react-navigation/native": "^5.8.7",
|
||||
"@react-navigation/stack": "^5.12.4",
|
||||
"axios": "^0.21.0",
|
||||
"dinero.js": "^1.8.1",
|
||||
"expo": "^39.0.4",
|
||||
"expo-camera": "~9.0.0",
|
||||
@@ -26,6 +27,7 @@
|
||||
"expo-permissions": "~9.3.0",
|
||||
"expo-sqlite": "~8.4.0",
|
||||
"expo-status-bar": "~1.0.2",
|
||||
"expo-video-thumbnails": "~4.3.0",
|
||||
"firebase": "7.9.0",
|
||||
"formik": "^2.2.3",
|
||||
"graphql": "^15.4.0",
|
||||
@@ -37,6 +39,7 @@
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
|
||||
"react-native-easy-grid": "^0.2.2",
|
||||
"react-native-gesture-handler": "~1.7.0",
|
||||
"react-native-image-base64": "^0.1.4",
|
||||
"react-native-indicators": "^0.17.0",
|
||||
"react-native-reanimated": "~1.13.0",
|
||||
"react-native-safe-area-context": "3.1.4",
|
||||
|
||||
@@ -16,7 +16,7 @@ export const documentUploadStart = (jobId) => ({
|
||||
});
|
||||
|
||||
export const documentUploadSuccess = (jobId) => ({
|
||||
type: AppActionTypes.DOCUMNET_UPLOAD_SUCCESS,
|
||||
type: AppActionTypes.DOCUMENT_UPLOAD_SUCCESS,
|
||||
payload: jobId,
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const appReducer = (state = INITIAL_STATE, action) => {
|
||||
documentUploadError: null,
|
||||
documentUploadInProgress: action.payload,
|
||||
};
|
||||
case AppActionTypes.DOCUMNET_UPLOAD_SUCCESS:
|
||||
case AppActionTypes.DOCUMENT_UPLOAD_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
documentUploadError: null,
|
||||
|
||||
@@ -2,7 +2,7 @@ const AppActionTypes = {
|
||||
SET_CAMERA_JOB_ID: "SET_CAMERA_JOB_ID",
|
||||
SET_CAMERA_JOB: "SET_CAMERA_JOB",
|
||||
DOCUMENT_UPLOAD_START: "DOCUMENT_UPLOAD_START",
|
||||
DOCUMNET_UPLOAD_SUCCESS: "DOCUMNET_UPLOAD_SUCCESS",
|
||||
DOCUMENT_UPLOAD_SUCCESS: "DOCUMENT_UPLOAD_SUCCESS",
|
||||
DOCUMENT_UPLOAD_FAILURE: "DOCUMENT_UPLOAD_FAILURE",
|
||||
};
|
||||
export default AppActionTypes;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { all, call, takeLatest } from "redux-saga/effects";
|
||||
import PhotosActionTypes from "./photos.types";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { all, call, select, takeLatest } from "redux-saga/effects";
|
||||
import { handleUpload } from "../../util/document-upload.utility";
|
||||
import PhotosActionTypes from "./photos.types";
|
||||
|
||||
export function* onRemoveAllPhotos() {
|
||||
yield takeLatest(PhotosActionTypes.REMOVE_ALL_PHOTOS, removeAllPhotosAction);
|
||||
@@ -20,11 +21,58 @@ export function* removeAllPhotosAction() {
|
||||
console.log("All photos deleted.");
|
||||
} catch (error) {
|
||||
console.log("Saga Error: onRemoveAllPhotos", error);
|
||||
//yield put(signInFailure(error));
|
||||
//logImEXEvent("redux_sign_in_failure", { user: email, error });
|
||||
}
|
||||
}
|
||||
|
||||
export function* onUploadAllPhotos() {
|
||||
yield takeLatest(
|
||||
PhotosActionTypes.UPLOAD_ALL_PHOTOS_START,
|
||||
uploadAllPhotosAction
|
||||
);
|
||||
}
|
||||
export function* uploadAllPhotosAction() {
|
||||
try {
|
||||
const photos = yield select((state) => state.photos.photos);
|
||||
const bodyshop = yield select((state) => state.user.bodyshop);
|
||||
const user = yield select((state) => state.user);
|
||||
const actions = [];
|
||||
photos.forEach(async (p) =>
|
||||
actions.push(
|
||||
handleUpload(
|
||||
{
|
||||
file: await (await fetch(p.uri)).blob(),
|
||||
|
||||
onError: (props) => {
|
||||
console.log("Error Callback", props);
|
||||
},
|
||||
onProgress: (props) => {
|
||||
console.log("Progress Calback", props);
|
||||
},
|
||||
onSuccess: (props) => {
|
||||
console.log("Success Calback", props);
|
||||
},
|
||||
},
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
jobId: p.jobId,
|
||||
uploaded_by: user.currentUser.email,
|
||||
callback: (props) => {
|
||||
console.log("Context Callback", props);
|
||||
},
|
||||
photo: {
|
||||
...p,
|
||||
name: p.uri.substring(p.uri.lastIndexOf("/") + 1),
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
);
|
||||
yield Promise.all(actions);
|
||||
} catch (error) {
|
||||
console.log("Saga Error: onRemoveAllPhotos", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function* photosSagas() {
|
||||
yield all([call(onRemoveAllPhotos)]);
|
||||
yield all([call(onRemoveAllPhotos), call(onUploadAllPhotos)]);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ const INITIAL_STATE = {
|
||||
authorized: null,
|
||||
},
|
||||
bodyshop: null,
|
||||
fingerprint: null,
|
||||
signingIn: false,
|
||||
error: null,
|
||||
conflict: false,
|
||||
};
|
||||
|
||||
const userReducer = (state = INITIAL_STATE, action) => {
|
||||
|
||||
27
util/CleanAxios.js
Normal file
27
util/CleanAxios.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import axios from "axios";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
axios.defaults.baseURL =
|
||||
process.env.REACT_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
|
||||
}
|
||||
|
||||
export const axiosAuthInterceptorId = axios.interceptors.request.use(
|
||||
async (config) => {
|
||||
if (!config.headers.Authorization) {
|
||||
const token =
|
||||
auth.currentUser && (await auth.currentUser.getIdToken(true));
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
const cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
export default cleanAxios;
|
||||
184
util/document-upload.utility.js
Normal file
184
util/document-upload.utility.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import axios from "axios";
|
||||
import env from "../env";
|
||||
import { client } from "../graphql/client";
|
||||
import { INSERT_NEW_DOCUMENT } from "../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "./CleanAxios";
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
//Required to prevent headers from getting set and rejected from Cloudinary.
|
||||
var cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
export const handleUpload = (ev, context) => {
|
||||
const { onError, onSuccess, onProgress } = ev;
|
||||
const { bodyshop, jobId } = context;
|
||||
|
||||
let key = `${bodyshop.id}/${jobId}/${ev.file.data.name.replace(
|
||||
/\.[^/.]+$/,
|
||||
""
|
||||
)}`;
|
||||
|
||||
uploadToCloudinary(
|
||||
key,
|
||||
ev.file.type,
|
||||
ev.file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context
|
||||
);
|
||||
};
|
||||
|
||||
export const uploadToCloudinary = async (
|
||||
key,
|
||||
fileType,
|
||||
file,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context
|
||||
) => {
|
||||
const {
|
||||
bodyshop,
|
||||
jobId,
|
||||
billId,
|
||||
uploaded_by,
|
||||
callback,
|
||||
tagsArray,
|
||||
photo,
|
||||
} = context;
|
||||
|
||||
//Set variables for getting the signed URL.
|
||||
let timestamp = Math.floor(Date.now() / 1000);
|
||||
let public_id = key;
|
||||
let tags = `${bodyshop.textid},${
|
||||
tagsArray ? tagsArray.map((tag) => `${tag},`) : ""
|
||||
}`;
|
||||
// let eager = process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS;
|
||||
|
||||
//Get the signed url.
|
||||
let signedURLResponse;
|
||||
try {
|
||||
signedURLResponse = await axios.post(
|
||||
"https://d2ea3cff6920.ngrok.io/media/sign",
|
||||
{
|
||||
public_id: public_id,
|
||||
tags: tags,
|
||||
timestamp: timestamp,
|
||||
upload_preset: "incoming_upload",
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("ERROR GETTING SIGNED URL", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
||||
if (!!onError) onError(signedURLResponse.statusText);
|
||||
// notification["error"]({
|
||||
// message: i18n.t("documents.errors.getpresignurl", {
|
||||
// message: signedURLResponse.statusText,
|
||||
// }),
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
//Build request to end to cloudinary.
|
||||
var signature = signedURLResponse.data;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
onUploadProgress: (e) => {
|
||||
if (!!onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||
},
|
||||
};
|
||||
const formData = new FormData();
|
||||
|
||||
console.log("Sending!", {
|
||||
uri: photo.uri,
|
||||
type: fileType,
|
||||
name: file.data.name,
|
||||
});
|
||||
formData.append("file", {
|
||||
uri: photo.uri,
|
||||
type: fileType,
|
||||
name: file.data.name,
|
||||
});
|
||||
console.log("Applying lower quality transforms.");
|
||||
formData.append("upload_preset", "incoming_upload");
|
||||
|
||||
formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY);
|
||||
formData.append("public_id", public_id);
|
||||
formData.append("tags", tags);
|
||||
formData.append("timestamp", timestamp);
|
||||
formData.append("signature", signature);
|
||||
|
||||
//Upload request to Cloudinary
|
||||
let cloudinaryUploadResponse;
|
||||
try {
|
||||
cloudinaryUploadResponse = await cleanAxios.post(
|
||||
`${env.REACT_APP_CLOUDINARY_ENDPOINT}/upload`,
|
||||
formData,
|
||||
{
|
||||
...options,
|
||||
}
|
||||
);
|
||||
console.log("Cloudinary Upload Response", cloudinaryUploadResponse.data);
|
||||
} catch (error) {
|
||||
console.log("CLOUDINARY error", error, cloudinaryUploadResponse);
|
||||
}
|
||||
|
||||
if (cloudinaryUploadResponse.status !== 200) {
|
||||
console.log(
|
||||
"Error uploading to cloudinary.",
|
||||
cloudinaryUploadResponse.statusText,
|
||||
cloudinaryUploadResponse
|
||||
);
|
||||
if (!!onError) onError(cloudinaryUploadResponse.statusText);
|
||||
// notification["error"]({
|
||||
// message: i18n.t("documents.errors.insert", {
|
||||
// message: cloudinaryUploadResponse.statusText,
|
||||
// }),
|
||||
// });
|
||||
return;
|
||||
}
|
||||
|
||||
//Insert the document with the matching key.
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
jobid: jobId,
|
||||
uploaded_by: uploaded_by,
|
||||
key: key,
|
||||
billid: billId,
|
||||
type: fileType,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
if (!documentInsert.errors) {
|
||||
if (!!onSuccess)
|
||||
onSuccess({
|
||||
uid: documentInsert.data.insert_documents.returning[0].id,
|
||||
name: documentInsert.data.insert_documents.returning[0].name,
|
||||
status: "done",
|
||||
key: documentInsert.data.insert_documents.returning[0].key,
|
||||
});
|
||||
// notification["success"]({
|
||||
// message: i18n.t("documents.successes.insert"),
|
||||
// });
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
if (!!onError) onError(JSON.stringify(documentInsert.errors));
|
||||
// notification["error"]({
|
||||
// message: i18n.t("documents.errors.insert", {
|
||||
// message: JSON.stringify(JSON.stringify(documentInsert.errors)),
|
||||
// }),
|
||||
// });
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -3311,6 +3311,11 @@ expo-status-bar@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.0.2.tgz#2441a77c56be31597898337b0d086981f2adefd8"
|
||||
integrity sha512-5313u744GcLzCadxIPXyTkYw77++UXv1dXCuhYDxDbtsEf93iMra7WSvzyE8a7mRQLIIPRuGnBOdrL/V1C7EOQ==
|
||||
|
||||
expo-video-thumbnails@~4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/expo-video-thumbnails/-/expo-video-thumbnails-4.3.0.tgz#8381a73616a3c36604d8375d5b634d525a7f85a8"
|
||||
integrity sha512-qe4bvSlTP0FNGbkg99vCo/kijSKXqXEFlLM3e5phArdmmY0c7NNevQRWHHQHSguGaKp7HBZ7LX0CyNAjiH8SpA==
|
||||
|
||||
expo@^39.0.4:
|
||||
version "39.0.4"
|
||||
resolved "https://registry.yarnpkg.com/expo/-/expo-39.0.4.tgz#320b7453ac055fc37c64942d5ba442f4e2781993"
|
||||
@@ -5799,7 +5804,7 @@ react-native-drawer@2.5.1:
|
||||
prop-types "^15.5.8"
|
||||
tween-functions "^1.0.1"
|
||||
|
||||
react-native-easy-grid@0.2.2:
|
||||
react-native-easy-grid@0.2.2, react-native-easy-grid@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-easy-grid/-/react-native-easy-grid-0.2.2.tgz#f0be33620be1ebe2d2295918eb58b0a27e8272ab"
|
||||
integrity sha512-MlYrNIldnEMKn6TVatQN1P64GoVlwGIuz+8ncdfJ0Wq/xtzUkQwlil8Uksyp7MhKfENE09MQnGNcba6Mx3oSAA==
|
||||
|
||||
Reference in New Issue
Block a user