diff --git a/components/screen-camera/screen-camera.component.jsx b/components/screen-camera/screen-camera.component.jsx
index 40d3634..e562d88 100644
--- a/components/screen-camera/screen-camera.component.jsx
+++ b/components/screen-camera/screen-camera.component.jsx
@@ -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 No access to camera;
}
- const { hasCameraPermission, flashMode, cameraType, capturing } = state;
+ const { flashMode, cameraType, capturing } = state;
return (
diff --git a/components/screen-media-cache/screen-media-cache.component.jsx b/components/screen-media-cache/screen-media-cache.component.jsx
index 3d2f827..58e2884 100644
--- a/components/screen-media-cache/screen-media-cache.component.jsx
+++ b/components/screen-media-cache/screen-media-cache.component.jsx
@@ -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 (
This is the media cache screen.
+
{photos.length}
item.id}
renderItem={(object) => (
{object.item.uri}
-
+ {!object.item.video && (
+
+ )}
)}
- //ItemSeparatorComponent={FlatListItemSeparator}
/>
);
diff --git a/env.js b/env.js
new file mode 100644
index 0000000..1f9ff48
--- /dev/null
+++ b/env.js
@@ -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);
diff --git a/graphql/bodyshop.queries.js b/graphql/bodyshop.queries.js
index 8f332f1..eb3b146 100644
--- a/graphql/bodyshop.queries.js
+++ b/graphql/bodyshop.queries.js
@@ -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
diff --git a/graphql/documents.queries.js b/graphql/documents.queries.js
new file mode 100644
index 0000000..361eb11
--- /dev/null
+++ b/graphql/documents.queries.js
@@ -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
+ }
+ }
+ }
+`;
diff --git a/package-lock.json b/package-lock.json
index f820008..17165f9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index fa388fd..67e11e3 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/redux/app/app.actions.js b/redux/app/app.actions.js
index 7c906be..b9af5db 100644
--- a/redux/app/app.actions.js
+++ b/redux/app/app.actions.js
@@ -16,7 +16,7 @@ export const documentUploadStart = (jobId) => ({
});
export const documentUploadSuccess = (jobId) => ({
- type: AppActionTypes.DOCUMNET_UPLOAD_SUCCESS,
+ type: AppActionTypes.DOCUMENT_UPLOAD_SUCCESS,
payload: jobId,
});
diff --git a/redux/app/app.reducer.js b/redux/app/app.reducer.js
index 0ed3ae3..c22f6e9 100644
--- a/redux/app/app.reducer.js
+++ b/redux/app/app.reducer.js
@@ -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,
diff --git a/redux/app/app.types.js b/redux/app/app.types.js
index 2cf7f79..d706bb7 100644
--- a/redux/app/app.types.js
+++ b/redux/app/app.types.js
@@ -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;
diff --git a/redux/photos/photos.sagas.js b/redux/photos/photos.sagas.js
index e6e6a5d..6a53ff3 100644
--- a/redux/photos/photos.sagas.js
+++ b/redux/photos/photos.sagas.js
@@ -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)]);
}
diff --git a/redux/user/user.reducer.js b/redux/user/user.reducer.js
index 69eab5f..a5cf187 100644
--- a/redux/user/user.reducer.js
+++ b/redux/user/user.reducer.js
@@ -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) => {
diff --git a/util/CleanAxios.js b/util/CleanAxios.js
new file mode 100644
index 0000000..de36f24
--- /dev/null
+++ b/util/CleanAxios.js
@@ -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;
diff --git a/util/document-upload.utility.js b/util/document-upload.utility.js
new file mode 100644
index 0000000..75bfaa0
--- /dev/null
+++ b/util/document-upload.utility.js
@@ -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;
+ }
+};
diff --git a/yarn.lock b/yarn.lock
index 29126ba..90de6f5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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==