First prototype of image upload working. IO-397 IO-398

This commit is contained in:
Patrick Fic
2020-11-17 13:39:31 -08:00
parent 79ec14fe53
commit cd5f8af9e4
15 changed files with 395 additions and 19 deletions

View File

@@ -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 }}>

View File

@@ -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
View 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);

View File

@@ -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

View 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
View File

@@ -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",

View File

@@ -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",

View File

@@ -16,7 +16,7 @@ export const documentUploadStart = (jobId) => ({
});
export const documentUploadSuccess = (jobId) => ({
type: AppActionTypes.DOCUMNET_UPLOAD_SUCCESS,
type: AppActionTypes.DOCUMENT_UPLOAD_SUCCESS,
payload: jobId,
});

View File

@@ -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,

View File

@@ -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;

View File

@@ -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)]);
}

View File

@@ -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
View 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;

View 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;
}
};

View File

@@ -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==