Merge branch 'feature/sdk44'
* feature/sdk44: (29 commits) 1.3.5-6 - Prod Build - Add back filename. 1.3.7-5 - Production - Update logic and numbers. Remove package. 1.3.7-4 - Prod Build - Updated sentry logging. 1.3.7-3 - Test Build - Update Photo Viewer. 1.3.7-2 - Test Build - Added progress for fetching file size. Improve cloudinary media upload experience. Changed image viewer and added mobile feature check. 1.3.6-8 - Production Build - Resolve deletion. 1.3.6-7 Production Build - Resolve errors on local upload and deleting. 1.3.6-6 Production Build - Version updates for submission. 1.3.6-5 Resolve uploads for cloudinary. 1.3.6-4 Test Build - Add average upload for local upload. 1.3.6-3 Test Build - Refactor local uploads to go in bulk. 1.3.6-2 Test Build - Individual file uploads & cloudariny fixes. 1.3.6-1 Release - Toast for successful push. Remove IPA file. 1.3.6(1) Resolved local upload issues due to form type. Resolve video upload issues. Added local build configuration. Add IMS Token. ...
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,3 +12,7 @@ yarn-error.log
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
|
||||
*.ipa
|
||||
*.aab
|
||||
38
App.js
38
App.js
@@ -8,12 +8,23 @@ import ScreenMainComponent from "./components/screen-main/screen-main.component"
|
||||
import { logImEXEvent } from "./firebase/firebase.analytics";
|
||||
import { client } from "./graphql/client";
|
||||
import { persistor, store } from "./redux/store";
|
||||
import "intl";
|
||||
import "intl/locale-data/jsonp/en";
|
||||
import "./translations/i18n";
|
||||
|
||||
import "expo-asset";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
Sentry.init({
|
||||
dsn: "https://8d6c3de1940a4e4f8b81cf4d2150bdea@o492140.ingest.sentry.io/5558869",
|
||||
enableInExpoDevelopment: true,
|
||||
// debug: true, // Sentry will try to print out useful debugging information if something goes wrong with sending an event. Set this to `false` in production.
|
||||
tracesSampleRate: 0.2,
|
||||
integrations: [
|
||||
new Sentry.Native.ReactNativeTracing({
|
||||
tracingOrigins: ["localhost", "imex.online", "cloudinary.com", /^\//],
|
||||
// ... other options
|
||||
}),
|
||||
],
|
||||
debug: true, // Sentry will try to print out useful debugging information if something goes wrong with sending an event. Set this to `false` in production.
|
||||
});
|
||||
|
||||
Sentry.Native.nativeCrash();
|
||||
@@ -22,7 +33,7 @@ const theme = {
|
||||
...DefaultTheme,
|
||||
colors: {
|
||||
...DefaultTheme.colors,
|
||||
primary: "dodgerblue",
|
||||
primary: "#1890ff",
|
||||
accent: "tomato",
|
||||
},
|
||||
};
|
||||
@@ -34,15 +45,18 @@ export default class App extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<PersistGate persistor={persistor}>
|
||||
<ApolloProvider client={client}>
|
||||
<PaperProvider theme={theme}>
|
||||
<ScreenMainComponent />
|
||||
</PaperProvider>
|
||||
</ApolloProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
<SafeAreaProvider>
|
||||
<Provider store={store}>
|
||||
<PersistGate persistor={persistor}>
|
||||
<ApolloProvider client={client}>
|
||||
<PaperProvider theme={theme}>
|
||||
<ScreenMainComponent />
|
||||
<Toast />
|
||||
</PaperProvider>
|
||||
</ApolloProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
25
app.json
25
app.json
@@ -2,24 +2,25 @@
|
||||
"expo": {
|
||||
"name": "ImEX Mobile",
|
||||
"slug": "imexmobile",
|
||||
"version": "1.2.3",
|
||||
"extra": { "expover": "1" },
|
||||
"version": "1.3.7",
|
||||
"extra": {
|
||||
"expover": "6"
|
||||
},
|
||||
"orientation": "default",
|
||||
"icon": "./assets/logo192noa.png",
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.imex.imexmobile",
|
||||
"buildNumber": "1.2.3",
|
||||
"buildNumber": "6",
|
||||
"googleServicesFile": "./GoogleService-Info.plist"
|
||||
},
|
||||
"android": {
|
||||
"package": "com.imex.imexmobile",
|
||||
"versionCode": 1020300,
|
||||
"versionCode": 1100019,
|
||||
"googleServicesFile": "./google-services.json"
|
||||
},
|
||||
"splash": {
|
||||
"image": "./assets/Splash.png",
|
||||
|
||||
"backgroundColor": "#efefef"
|
||||
},
|
||||
"notification": {
|
||||
@@ -29,7 +30,6 @@
|
||||
"fallbackToCacheTimeout": 0
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
|
||||
"web": {
|
||||
"favicon": "./assets/logo192noa.png",
|
||||
"config": {
|
||||
@@ -57,6 +57,17 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"sentry-expo",
|
||||
[
|
||||
"expo-media-library",
|
||||
{
|
||||
"photosPermission": "Allow $(PRODUCT_NAME) to access your photos.",
|
||||
"savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.",
|
||||
"isAccessMediaLocationEnabled": "true"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,27 @@
|
||||
<folder_node>
|
||||
<name>app</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>nomobileaccess</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>title</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -1216,6 +1237,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>search</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
</children>
|
||||
</folder_node>
|
||||
<folder_node>
|
||||
@@ -1341,6 +1383,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>localserver</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>nomedia</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -20,14 +20,24 @@ export default function JobDocumentsComponent({ job, loading, refetch }) {
|
||||
|
||||
const fullphotos = useMemo(
|
||||
() =>
|
||||
job.documents.map((doc) => {
|
||||
job.documents.map((doc, idx) => {
|
||||
return {
|
||||
id: idx,
|
||||
videoUrl:
|
||||
DetermineFileType(doc.type) === "video" && GenerateSrcUrl(doc),
|
||||
source:
|
||||
DetermineFileType(doc.type) === "video"
|
||||
? { uri: GenerateThumbUrl(doc) }
|
||||
: { uri: GenerateSrcUrl(doc) },
|
||||
url:
|
||||
DetermineFileType(doc.type) === "video"
|
||||
? GenerateThumbUrl(doc)
|
||||
: GenerateSrcUrl(doc),
|
||||
uri:
|
||||
DetermineFileType(doc.type) === "video"
|
||||
? GenerateThumbUrl(doc)
|
||||
: GenerateSrcUrl(doc),
|
||||
thumbUrl: GenerateThumbUrl(doc),
|
||||
};
|
||||
}),
|
||||
[job.documents]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import React from "react";
|
||||
import { RefreshControl, View, Text } from "react-native";
|
||||
import { FlatList } from "react-native-gesture-handler";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FlatList, RefreshControl, Text, View } from "react-native";
|
||||
import { Button, Searchbar, Title } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||
@@ -9,8 +10,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ErrorDisplay from "../error-display/error-display.component";
|
||||
import JobListItem from "../job-list-item/job-list-item.component";
|
||||
import LoadingDisplay from "../loading-display/loading-display.component";
|
||||
import { Title, Button, Searchbar } from "react-native-paper";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
@@ -23,6 +23,7 @@ export function JobListComponent({ bodyshop }) {
|
||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
|
||||
},
|
||||
skip: !bodyshop,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
const onRefresh = async () => {
|
||||
@@ -32,7 +33,6 @@ export function JobListComponent({ bodyshop }) {
|
||||
|
||||
if (loading) return <LoadingDisplay />;
|
||||
if (error) return <ErrorDisplay errorMessage={error.message} />;
|
||||
|
||||
if (data && data.jobs && data.jobs.length === 0)
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
@@ -74,7 +74,11 @@ export function JobListComponent({ bodyshop }) {
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Searchbar onChangeText={onChangeSearch} value={searchQuery} />
|
||||
<Searchbar
|
||||
onChangeText={onChangeSearch}
|
||||
value={searchQuery}
|
||||
placeholder={t("joblist.labels.search")}
|
||||
/>
|
||||
<FlatList
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={onRefresh} />
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { ProgressBar } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
import {
|
||||
selectCurrentCameraJobId,
|
||||
selectDeleteAfterUpload,
|
||||
} from "../../redux/app/app.selectors";
|
||||
import * as Sentry from "sentry-expo";
|
||||
|
||||
import { formatBytes } from "../../util/document-upload.utility";
|
||||
import { handleLocalUpload } from "../../util/local-document-upload.utility";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedCameraJobId: selectCurrentCameraJobId,
|
||||
deleteAfterUpload: selectDeleteAfterUpload,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(UploadProgress);
|
||||
|
||||
export function UploadProgress({
|
||||
selectedCameraJobId,
|
||||
deleteAfterUpload,
|
||||
uploads,
|
||||
setUploads,
|
||||
forceRerender,
|
||||
}) {
|
||||
const [progress, setProgress] = useState({
|
||||
loading: false,
|
||||
uploadInProgress: false,
|
||||
speed: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
//Set the state of uploads to do.
|
||||
if (uploads) {
|
||||
beginUploads(uploads);
|
||||
setUploads(null);
|
||||
}
|
||||
}, [uploads]);
|
||||
|
||||
async function handleOnSuccess({ duration, data }) {
|
||||
//If it's not in production, show a toast with the time.
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: ` Upload completed in ${duration}.`,
|
||||
//
|
||||
// text2: duration,
|
||||
});
|
||||
if (deleteAfterUpload) {
|
||||
try {
|
||||
await MediaLibrary.deleteAssetsAsync(data);
|
||||
} catch (error) {
|
||||
console.log("Unable to delete picture.", error);
|
||||
Sentry.Native.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
logImEXEvent("imexmobile_successful_upload");
|
||||
forceRerender();
|
||||
setProgress({ ...progress, speed: 0, percent: 1, uploadInProgress: false });
|
||||
}
|
||||
|
||||
function handleOnProgress({ percent, loaded }) {
|
||||
setProgress((progress) => ({
|
||||
...progress,
|
||||
speed: loaded - progress.loaded,
|
||||
loaded: loaded,
|
||||
percent,
|
||||
}));
|
||||
}
|
||||
|
||||
function handleOnError({ assetid, error }) {
|
||||
logImEXEvent("imexmobile_upload_documents_error");
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Unable to upload documents.",
|
||||
text2: error,
|
||||
autoHide: false,
|
||||
});
|
||||
setProgress({
|
||||
speed: 0,
|
||||
percent: 1,
|
||||
uploadInProgress: false,
|
||||
});
|
||||
}
|
||||
|
||||
const beginUploads = async (data) => {
|
||||
//Validate to make sure the totals for the file sizes do not exceed the total on the job.
|
||||
setProgress({
|
||||
percent: 0,
|
||||
loaded: 0,
|
||||
uploadInProgress: true,
|
||||
start: new Date(),
|
||||
average: 0,
|
||||
});
|
||||
|
||||
await handleLocalUpload({
|
||||
files: data,
|
||||
onError: ({ assetid, error }) => handleOnError({ assetid, error }),
|
||||
onProgress: ({ percent, loaded }) =>
|
||||
handleOnProgress({ percent, loaded }),
|
||||
onSuccess: ({ duration }) => handleOnSuccess({ duration, data }),
|
||||
context: {
|
||||
jobid:
|
||||
selectedCameraJobId !== "temp" ? selectedCameraJobId : "temporary",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={progress.uploadInProgress}
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => {
|
||||
Alert.alert("Cancel?", "Do you want to abort the upload?", [
|
||||
{
|
||||
text: "Yes",
|
||||
onPress: () => {
|
||||
setUploads(null);
|
||||
setProgress(null);
|
||||
},
|
||||
},
|
||||
{ text: "No" },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modal}>
|
||||
<ActivityIndicator style={{ alignSelf: "center", marginTop: 16 }} />
|
||||
<ProgressBar
|
||||
progress={progress.percent}
|
||||
style={{ alignSelf: "center", marginTop: 16 }}
|
||||
color={progress.percent === 1 ? "green" : "blue"}
|
||||
/>
|
||||
<Text style={{ alignSelf: "center", marginTop: 16 }}>{`${formatBytes(
|
||||
progress.speed
|
||||
)}/sec`}</Text>
|
||||
<Text
|
||||
style={{ alignSelf: "center", marginTop: 16 }}
|
||||
>{`Avg. ${formatBytes(
|
||||
progress.loaded / ((new Date() - progress.start) / 1000)
|
||||
)}/sec`}</Text>
|
||||
<Text
|
||||
style={{ alignSelf: "center", marginTop: 16 }}
|
||||
>{`Total Uploaded ${formatBytes(progress.loaded)}`}</Text>
|
||||
<Text style={{ alignSelf: "center", marginTop: 16 }}>{`Duration ${(
|
||||
(new Date() - progress.start) /
|
||||
1000
|
||||
).toFixed(1)} sec`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
},
|
||||
modal: {
|
||||
// flex: 1,
|
||||
display: "flex",
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 20,
|
||||
padding: 18,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 4,
|
||||
elevation: 5,
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Video } from "expo-av";
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Dimensions,
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import Gallery from "react-native-image-gallery";
|
||||
import { SafeAreaView } from "react-native";
|
||||
import React from "react";
|
||||
|
||||
import ImageView from "react-native-image-viewing";
|
||||
|
||||
export default function MediaCacheOverlay({
|
||||
photos,
|
||||
@@ -16,61 +10,73 @@ export default function MediaCacheOverlay({
|
||||
imgIndex,
|
||||
setImgIndex,
|
||||
}) {
|
||||
const [currentIndex, setcurrentIndex] = useState(0);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const videoRef = React.useRef(null);
|
||||
//const videoRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onDismiss={() => setPreviewVisible(false)}
|
||||
onRequestClose={() => setPreviewVisible(false)}
|
||||
visible={previewVisible}
|
||||
transparent={false}
|
||||
>
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: "black" }}>
|
||||
<Gallery
|
||||
initialPage={imgIndex}
|
||||
images={photos}
|
||||
onPageScroll={({ position }) => setcurrentIndex(position)}
|
||||
onPageScrollStateChanged={(state) =>
|
||||
state === "idle" ? setDragging(false) : setDragging(true)
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={{ position: "absolute" }}
|
||||
onPress={() => setPreviewVisible(false)}
|
||||
>
|
||||
<Ionicons
|
||||
name="ios-close"
|
||||
size={64}
|
||||
color="dodgerblue"
|
||||
style={{ margin: 20 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{!dragging && photos[currentIndex] && photos[currentIndex].videoUrl && (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: Dimensions.get("window").width / 2 - 32,
|
||||
top: Dimensions.get("window").height / 2 - 32,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
onPress={async () => {
|
||||
await videoRef.current.loadAsync(
|
||||
{ uri: photos[currentIndex].videoUrl },
|
||||
{},
|
||||
false
|
||||
);
|
||||
videoRef.current.presentFullscreenPlayer();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="play" size={64} color="white" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
<Video ref={videoRef} useNativeControls />
|
||||
</Modal>
|
||||
<SafeAreaView>
|
||||
<ImageView
|
||||
onRequestClose={() => setPreviewVisible(false)}
|
||||
visible={previewVisible}
|
||||
images={photos}
|
||||
imageIndex={imgIndex}
|
||||
onImageIndexChange={(...props) => {
|
||||
console.log(props);
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <Modal
|
||||
// onDismiss={() => setPreviewVisible(false)}
|
||||
// onRequestClose={() => setPreviewVisible(false)}
|
||||
// visible={previewVisible}
|
||||
// transparent={false}
|
||||
// >
|
||||
// <SafeAreaView style={{ flex: 1, backgroundColor: "black" }}>
|
||||
// <Gallery
|
||||
// initialPage={imgIndex}
|
||||
|
||||
// images={photos}
|
||||
// onPageScroll={({ position }) => setcurrentIndex(position)}
|
||||
// onPageScrollStateChanged={(state) =>
|
||||
// state === "idle" ? setDragging(false) : setDragging(true)
|
||||
// }
|
||||
// />
|
||||
// <TouchableOpacity
|
||||
// style={{ position: "absolute" }}
|
||||
// onPress={() => setPreviewVisible(false)}
|
||||
// >
|
||||
// <Ionicons
|
||||
// name="ios-close"
|
||||
// size={64}
|
||||
// color="dodgerblue"
|
||||
// style={{ margin: 20 }}
|
||||
// />
|
||||
// </TouchableOpacity>
|
||||
// {!dragging && photos[currentIndex] && photos[currentIndex].videoUrl && (
|
||||
// <TouchableOpacity
|
||||
// style={{
|
||||
// position: "absolute",
|
||||
// left: Dimensions.get("window").width / 2 - 32,
|
||||
// top: Dimensions.get("window").height / 2 - 32,
|
||||
// justifyContent: "center",
|
||||
// alignItems: "center",
|
||||
// }}
|
||||
// onPress={async () => {
|
||||
// await videoRef.current.loadAsync(
|
||||
// { uri: photos[currentIndex].videoUrl },
|
||||
// {},
|
||||
// false
|
||||
// );
|
||||
// videoRef.current.presentFullscreenPlayer();
|
||||
// }}
|
||||
// >
|
||||
// <Ionicons name="play" size={64} color="white" />
|
||||
// </TouchableOpacity>
|
||||
// )}
|
||||
// </SafeAreaView>
|
||||
// <Video ref={videoRef} useNativeControls />
|
||||
// </Modal>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -10,8 +10,18 @@ import JobLines from "../job-lines/job-lines.component";
|
||||
import JobNotes from "../job-notes/job-notes.component";
|
||||
import JobTombstone from "../job-tombstone/job-tombstone.component";
|
||||
import LoadingDisplay from "../loading-display/loading-display.component";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ScreenJobDetail);
|
||||
|
||||
export default function ScreenJobDetail({ route }) {
|
||||
export function ScreenJobDetail({ bodyshop, route }) {
|
||||
const {
|
||||
params: { jobId },
|
||||
} = route;
|
||||
@@ -45,12 +55,16 @@ export default function ScreenJobDetail({ route }) {
|
||||
loading: loading,
|
||||
refetch: refetch,
|
||||
}),
|
||||
documents: () =>
|
||||
JobDocuments({
|
||||
job: data.jobs_by_pk,
|
||||
loading: loading,
|
||||
refetch: refetch,
|
||||
}),
|
||||
...(bodyshop.uselocalmediaserver
|
||||
? {}
|
||||
: {
|
||||
documents: () =>
|
||||
JobDocuments({
|
||||
job: data.jobs_by_pk,
|
||||
loading: loading,
|
||||
refetch: refetch,
|
||||
}),
|
||||
}),
|
||||
notes: () =>
|
||||
JobNotes({
|
||||
job: data.jobs_by_pk,
|
||||
@@ -63,7 +77,9 @@ export default function ScreenJobDetail({ route }) {
|
||||
const [routes] = React.useState([
|
||||
{ key: "job", title: t("jobdetail.labels.job") },
|
||||
{ key: "lines", title: t("jobdetail.labels.lines") },
|
||||
{ key: "documents", title: t("jobdetail.labels.documents") },
|
||||
...(bodyshop.uselocalmediaserver
|
||||
? []
|
||||
: [{ key: "documents", title: t("jobdetail.labels.documents") }]),
|
||||
{ key: "notes", title: t("jobdetail.labels.notes") },
|
||||
]);
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import i18n from "i18next";
|
||||
import moment from "moment";
|
||||
import React, { useEffect } from "react";
|
||||
import { Button } from "react-native-paper";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
@@ -107,6 +107,15 @@ const MoreStackNavigator = () => (
|
||||
const BottomTabsNavigator = () => (
|
||||
<BottomTabs.Navigator
|
||||
screenOptions={({ route }) => ({
|
||||
// tabBarActiveTintColor: "dodgerblue",
|
||||
// tabBarInactiveTintColor: "slategrey",
|
||||
// tabBarStyle: [
|
||||
// {
|
||||
// display: "flex",
|
||||
// },
|
||||
// null,
|
||||
// ],
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
tabBarIcon: ({ color, size }) => {
|
||||
let iconName;
|
||||
@@ -123,24 +132,27 @@ const BottomTabsNavigator = () => (
|
||||
return <Ionicons name={iconName} size={size} color={color} />;
|
||||
},
|
||||
})}
|
||||
tabBarOptions={{
|
||||
activeTintColor: "dodgerblue",
|
||||
inactiveTintColor: "slategrey",
|
||||
}}
|
||||
>
|
||||
<BottomTabs.Screen
|
||||
name="JobTab"
|
||||
options={{ title: i18n.t("joblist.titles.jobtab") }}
|
||||
options={{
|
||||
title: i18n.t("joblist.titles.jobtab"),
|
||||
|
||||
headerShown: false,
|
||||
}}
|
||||
component={JobStackNavigator}
|
||||
/>
|
||||
<BottomTabs.Screen
|
||||
name="MediaBrowserTab"
|
||||
options={{ title: i18n.t("mediabrowser.titles.mediabrowsertab") }}
|
||||
options={{
|
||||
title: i18n.t("mediabrowser.titles.mediabrowsertab"),
|
||||
headerShown: false,
|
||||
}}
|
||||
component={MediaBrowserStackNavigator}
|
||||
/>
|
||||
<BottomTabs.Screen
|
||||
name="MoreTab"
|
||||
options={{ title: i18n.t("more.titles.moretab") }}
|
||||
options={{ title: i18n.t("more.titles.moretab"), headerShown: false }}
|
||||
component={MoreStackNavigator}
|
||||
/>
|
||||
</BottomTabs.Navigator>
|
||||
@@ -156,24 +168,33 @@ export function ScreenMainComponent({
|
||||
}, [checkUserSession]);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<NavigationContainer>
|
||||
{currentUser.authorized === null ? (
|
||||
<ScreenSplash />
|
||||
) : currentUser.authorized ? (
|
||||
bodyshop ? (
|
||||
<NavigationContainer>
|
||||
{currentUser.authorized === null ? (
|
||||
<ScreenSplash />
|
||||
) : currentUser.authorized ? (
|
||||
bodyshop ? (
|
||||
HasAccess(bodyshop) ? (
|
||||
<BottomTabsNavigator />
|
||||
) : (
|
||||
<ScreenSplash />
|
||||
<ScreenSplash noAccess />
|
||||
)
|
||||
) : (
|
||||
<ScreenSignIn />
|
||||
)}
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
<ScreenSplash />
|
||||
)
|
||||
) : (
|
||||
<ScreenSignIn />
|
||||
)}
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ScreenMainComponent);
|
||||
|
||||
function HasAccess({ features }) {
|
||||
if (features.mobile === undefined) return true;
|
||||
if (features.mobile === false) return false;
|
||||
const d = moment(moment(features.mobile));
|
||||
if (d.isValid()) return d.isAfter(moment());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { AssetsSelector } from "expo-images-picker";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { connect } from "react-redux";
|
||||
@@ -11,12 +11,16 @@ import CameraSelectJob from "../camera-select-job/camera-select-job.component";
|
||||
import JobSpaceAvailable from "../job-space-available/job-space-available.component";
|
||||
import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.component";
|
||||
import UploadProgress from "../upload-progress/upload-progress.component";
|
||||
import { MediaType } from "expo-media-library";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LocalUploadProgress from "../local-upload-progress/local-upload-progress.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedCameraJobId: selectCurrentCameraJobId,
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
export function ImageBrowserScreen({ selectedCameraJobId }) {
|
||||
export function ImageBrowserScreen({ bodyshop, selectedCameraJobId }) {
|
||||
const { t } = useTranslation();
|
||||
const [uploads, setUploads] = useState(null);
|
||||
const [tick, setTick] = useState(0);
|
||||
@@ -29,10 +33,105 @@ export function ImageBrowserScreen({ selectedCameraJobId }) {
|
||||
if (data.length !== 0) setUploads(data);
|
||||
};
|
||||
|
||||
const widgetErrors = useMemo(
|
||||
() => ({
|
||||
errorTextColor: "black",
|
||||
errorMessages: {
|
||||
hasErrorWithPermissions: "Please Allow media gallery permissions.",
|
||||
hasErrorWithLoading: "There was an error while loading images.",
|
||||
hasErrorWithResizing: "There was an error while loading images.",
|
||||
hasNoAssets: "No images found.",
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const widgetSettings = useMemo(
|
||||
() => ({
|
||||
getImageMetaData: false, // true might perform slower results but gives meta data and absolute path for ios users
|
||||
initialLoad: 100,
|
||||
assetsType: [MediaType.photo, MediaType.video],
|
||||
minSelection: 1,
|
||||
// maxSelection: 3,
|
||||
portraitCols: 4,
|
||||
landscapeCols: 4,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const widgetResize = useMemo(
|
||||
() => ({
|
||||
width: 50,
|
||||
compress: 0.7,
|
||||
base64: false,
|
||||
saveTo: "jpeg",
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const _textStyle = {
|
||||
color: "white",
|
||||
};
|
||||
|
||||
const _buttonStyle = {
|
||||
backgroundColor: "orange",
|
||||
borderRadius: 5,
|
||||
};
|
||||
|
||||
const widgetNavigator = useMemo(
|
||||
() => ({
|
||||
Texts: {
|
||||
finish: t("mediabrowser.actions.upload"),
|
||||
back: t("mediabrowser.actions.refresh"),
|
||||
selected: "selected",
|
||||
},
|
||||
midTextColor: "black",
|
||||
minSelection: 1,
|
||||
buttonTextStyle: styles.textStyle,
|
||||
buttonStyle: styles.buttonStyle,
|
||||
onBack: () => {
|
||||
forceRerender();
|
||||
},
|
||||
onSuccess: onDone,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const widgetStyles = useMemo(
|
||||
() => ({
|
||||
margin: 2,
|
||||
bgColor: "white",
|
||||
spinnerColor: "blue",
|
||||
widgetWidth: 99,
|
||||
videoIcon: {
|
||||
Component: Ionicons,
|
||||
iconName: "ios-videocam",
|
||||
color: "white",
|
||||
size: 20,
|
||||
},
|
||||
selectedIcon: {
|
||||
Component: Ionicons,
|
||||
iconName: "ios-checkmark-circle-outline",
|
||||
color: "white",
|
||||
bg: "rgba(35,35,35, 0.75)",
|
||||
size: 32,
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[styles.flex, styles.container]}>
|
||||
<CameraSelectJob />
|
||||
<JobSpaceAvailable jobid={selectedCameraJobId} key={`${tick}-space`} />
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<Text style={{ margin: 10 }}>
|
||||
{t("mediabrowser.labels.localserver", {
|
||||
url: bodyshop.localmediaserverhttp,
|
||||
})}
|
||||
</Text>
|
||||
) : (
|
||||
<JobSpaceAvailable jobid={selectedCameraJobId} key={`${tick}-space`} />
|
||||
)}
|
||||
<UploadDeleteSwitch />
|
||||
{!selectedCameraJobId && (
|
||||
<View
|
||||
@@ -49,65 +148,25 @@ export function ImageBrowserScreen({ selectedCameraJobId }) {
|
||||
<AssetsSelector
|
||||
style={{ flex: 1 }}
|
||||
key={tick}
|
||||
options={{
|
||||
assetsType: ["photo", "video"],
|
||||
margin: 3,
|
||||
portraitCols: 4,
|
||||
landscapeCols: 6,
|
||||
widgetWidth: 100,
|
||||
widgetBgColor: "white",
|
||||
selectedBgColor: "#adadad",
|
||||
spinnerColor: "#c8c8c8",
|
||||
videoIcon: {
|
||||
Component: Ionicons,
|
||||
iconName: "ios-videocam",
|
||||
color: "white",
|
||||
size: 20,
|
||||
},
|
||||
selectedIcon: {
|
||||
Component: Ionicons,
|
||||
iconName: "ios-checkmark-circle-outline",
|
||||
color: "white",
|
||||
bg: "rgba(35,35,35, 0.75)",
|
||||
size: 32,
|
||||
},
|
||||
defaultTopNavigator: {
|
||||
continueText: t("mediabrowser.actions.upload"),
|
||||
goBackText: t("mediabrowser.actions.refresh"),
|
||||
buttonStyle: styles.buttonStyle,
|
||||
textStyle: styles.textStyle,
|
||||
backFunction: () => {
|
||||
forceRerender();
|
||||
},
|
||||
doneFunction: onDone,
|
||||
},
|
||||
|
||||
noAssets: {
|
||||
Component: function NoAsset() {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
height: 200,
|
||||
marginHorizontal: 20,
|
||||
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ios-camera" size={72} />
|
||||
<Text style={{ textAlign: "center", marginTop: 10 }}>
|
||||
{t("mediabrowser.labels.nomedia")}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
},
|
||||
}}
|
||||
Settings={widgetSettings}
|
||||
Errors={widgetErrors}
|
||||
Styles={widgetStyles}
|
||||
Navigator={widgetNavigator}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<LocalUploadProgress
|
||||
uploads={uploads}
|
||||
setUploads={setUploads}
|
||||
forceRerender={forceRerender}
|
||||
/>
|
||||
) : (
|
||||
<UploadProgress
|
||||
uploads={uploads}
|
||||
setUploads={setUploads}
|
||||
forceRerender={forceRerender}
|
||||
/>
|
||||
)}
|
||||
<UploadProgress uploads={uploads} forceRerender={forceRerender} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -130,3 +189,60 @@ const styles = StyleSheet.create({
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ImageBrowserScreen);
|
||||
|
||||
// options={{
|
||||
// assetsType: ["photo", "video"],
|
||||
// margin: 3,
|
||||
// portraitCols: 4,
|
||||
// landscapeCols: 6,
|
||||
// widgetWidth: 100,
|
||||
// widgetBgColor: "white",
|
||||
// selectedBgColor: "#adadad",
|
||||
// spinnerColor: "#c8c8c8",
|
||||
// videoIcon: {
|
||||
// Component: Ionicons,
|
||||
// iconName: "ios-videocam",
|
||||
// color: "white",
|
||||
// size: 20,
|
||||
// },
|
||||
// selectedIcon: {
|
||||
// Component: Ionicons,
|
||||
// iconName: "ios-checkmark-circle-outline",
|
||||
// color: "white",
|
||||
// bg: "rgba(35,35,35, 0.75)",
|
||||
// size: 32,
|
||||
// },
|
||||
// defaultTopNavigator: {
|
||||
// continueText: t("mediabrowser.actions.upload"),
|
||||
// goBackText: t("mediabrowser.actions.refresh"),
|
||||
// buttonStyle: styles.buttonStyle,
|
||||
// textStyle: styles.textStyle,
|
||||
// backFunction: () => {
|
||||
// forceRerender();
|
||||
// },
|
||||
// doneFunction: onDone,
|
||||
// },
|
||||
|
||||
// noAssets: {
|
||||
// Component: function NoAsset() {
|
||||
// return (
|
||||
// <View
|
||||
// style={{
|
||||
// display: "flex",
|
||||
// flex: 1,
|
||||
// height: 200,
|
||||
// marginHorizontal: 20,
|
||||
|
||||
// alignItems: "center",
|
||||
// justifyContent: "center",
|
||||
// }}
|
||||
// >
|
||||
// <Ionicons name="ios-camera" size={72} />
|
||||
// <Text style={{ textAlign: "center", marginTop: 10 }}>
|
||||
// {t("mediabrowser.labels.nomedia")}
|
||||
// </Text>
|
||||
// </View>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
// }}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Constants from "expo-constants";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, View, Text } from "react-native";
|
||||
import { Title } from "react-native-paper";
|
||||
import { View, Text } from "react-native";
|
||||
import { Title, Button } from "react-native-paper";
|
||||
import { purgeStoredState } from "redux-persist";
|
||||
import SignOutButton from "../sign-out-button/sign-out-button.component";
|
||||
import * as Updates from "expo-updates";
|
||||
@@ -25,9 +25,9 @@ export default function ScreenSettingsComponent() {
|
||||
})}
|
||||
</Title>
|
||||
|
||||
<Text>{Updates.releaseChannel}</Text>
|
||||
<Text>Release Channel {Updates.releaseChannel}</Text>
|
||||
<SignOutButton />
|
||||
<Button title="Purge State" onPress={() => purgeStoredState()} />
|
||||
{/* <Button title="Purge State" onPress={() => purgeStoredState()} /> */}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SignIn({ emailSignInStart, signingIn }) {
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
marginTop: 80,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-evenly",
|
||||
@@ -47,43 +47,52 @@ export function SignIn({ emailSignInStart, signingIn }) {
|
||||
<Image style={localStyles.logo} source={Logo} />
|
||||
<Title>{t("app.title")}</Title>
|
||||
</View>
|
||||
<Formik initialValues={{ email: "", password: "" }} onSubmit={formSubmit}>
|
||||
{({ handleChange, handleBlur, handleSubmit, values }) => (
|
||||
<View>
|
||||
<TextInput
|
||||
label={t("signin.fields.email")}
|
||||
mode="outlined"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
onChangeText={handleChange("email")}
|
||||
onBlur={handleBlur("email")}
|
||||
value={values.email}
|
||||
style={[localStyles.input]}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={t("signin.fields.password")}
|
||||
mode="outlined"
|
||||
secureTextEntry={true}
|
||||
onChangeText={handleChange("password")}
|
||||
onBlur={handleBlur("password")}
|
||||
value={values.password}
|
||||
style={[localStyles.input]}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Formik
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={formSubmit}
|
||||
>
|
||||
{({ handleChange, handleBlur, handleSubmit, values }) => (
|
||||
<View>
|
||||
<TextInput
|
||||
label={t("signin.fields.email")}
|
||||
mode="outlined"
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
onChangeText={handleChange("email")}
|
||||
onBlur={handleBlur("email")}
|
||||
value={values.email}
|
||||
style={[localStyles.input]}
|
||||
/>
|
||||
|
||||
<SignInErrorAlertComponent />
|
||||
<Button mode="outlined" loading={signingIn} onPress={handleSubmit}>
|
||||
<Text>{t("signin.actions.signin")}</Text>
|
||||
</Button>
|
||||
<Text>
|
||||
{t("settings.labels.version", {
|
||||
number: Constants.manifest.version,
|
||||
})}
|
||||
{`${process.env.NODE_ENV || ""} ${Updates.releaseChannel || ""}`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Formik>
|
||||
<TextInput
|
||||
label={t("signin.fields.password")}
|
||||
mode="outlined"
|
||||
secureTextEntry={true}
|
||||
onChangeText={handleChange("password")}
|
||||
onBlur={handleBlur("password")}
|
||||
value={values.password}
|
||||
style={[localStyles.input]}
|
||||
/>
|
||||
|
||||
<SignInErrorAlertComponent />
|
||||
<Button
|
||||
mode="outlined"
|
||||
loading={signingIn}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
<Text>{t("signin.actions.signin")}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</Formik>
|
||||
</View>
|
||||
<Text style={{ padding: 10, alignSelf: "center" }}>
|
||||
{t("settings.labels.version", {
|
||||
number: Constants.manifest.version,
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, Image, StyleSheet, View } from "react-native";
|
||||
import { Title } from "react-native-paper";
|
||||
import { Title, Subheading, Divider } from "react-native-paper";
|
||||
import Logo from "../../assets/logo192.png";
|
||||
import SignOutButton from "../sign-out-button/sign-out-button.component";
|
||||
|
||||
export default function ScreenSplash() {
|
||||
export default function ScreenSplash({ noAccess }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View style={[localStyles.container]}>
|
||||
@@ -13,7 +14,17 @@ export default function ScreenSplash() {
|
||||
<Title>{t("app.title")}</Title>
|
||||
</View>
|
||||
|
||||
<ActivityIndicator color="dodgerblue" size="large" />
|
||||
{noAccess ? (
|
||||
<View style={[localStyles.logoContainer]}>
|
||||
<Subheading style={{ textAlign: "center" }}>
|
||||
{t("app.nomobileaccess")}
|
||||
</Subheading>
|
||||
<Divider />
|
||||
<SignOutButton />
|
||||
</View>
|
||||
) : (
|
||||
<ActivityIndicator color="dodgerblue" size="large" />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +39,7 @@ const localStyles = StyleSheet.create({
|
||||
logoContainer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
|
||||
margin: 10,
|
||||
alignItems: "center",
|
||||
},
|
||||
logo: { width: 175, height: 175, margin: 20 },
|
||||
|
||||
@@ -16,6 +16,7 @@ export function SignOutButton({ signOutStart }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Button
|
||||
style={{ margin: 8 }}
|
||||
onPress={() => signOutStart()}
|
||||
title={t("general.actions.signout")}
|
||||
/>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Modal,
|
||||
ScrollView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { ProgressBar } from "react-native-paper";
|
||||
import { Divider, ProgressBar } from "react-native-paper";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
@@ -27,6 +26,8 @@ import {
|
||||
selectCurrentUser,
|
||||
} from "../../redux/user/user.selectors";
|
||||
import { formatBytes, handleUpload } from "../../util/document-upload.utility";
|
||||
import Toast from "react-native-toast-message";
|
||||
import * as Sentry from "sentry-expo";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -43,12 +44,17 @@ export function UploadProgress({
|
||||
selectedCameraJobId,
|
||||
deleteAfterUpload,
|
||||
uploads,
|
||||
setUploads,
|
||||
forceRerender,
|
||||
}) {
|
||||
const [progress, setProgress] = useState({
|
||||
loading: false,
|
||||
uploadInProgress: false,
|
||||
speed: 0,
|
||||
totalToUpload: 0,
|
||||
totalUploaded: 0,
|
||||
startTime: null,
|
||||
totalFiles: 0,
|
||||
totalFilesCompleted: 0,
|
||||
currentFile: null,
|
||||
files: {}, //uri is the key, value is progress
|
||||
});
|
||||
|
||||
@@ -58,63 +64,79 @@ export function UploadProgress({
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
//Set the state of uploads to do.
|
||||
|
||||
if (uploads) onDone(uploads);
|
||||
if (uploads) {
|
||||
onDone(uploads);
|
||||
setUploads(null);
|
||||
}
|
||||
}, [uploads]);
|
||||
|
||||
//if (!uploads) return null;
|
||||
|
||||
function handleOnSuccess(id) {
|
||||
logImEXEvent("imexmobile_successful_upload");
|
||||
filesToDelete.push(id);
|
||||
function handleOnSuccess(asset) {
|
||||
//NEEDS REDO.
|
||||
filesToDelete.push(asset);
|
||||
setProgress((progress) => ({
|
||||
...progress,
|
||||
action: t("mediabrowser.labels.converting"),
|
||||
// totalUploaded: progress.totalToUpload + asset.size,
|
||||
totalFilesCompleted: progress.totalFilesCompleted + 1,
|
||||
files: {
|
||||
...progress.files,
|
||||
[id]: {
|
||||
...progress.files[id],
|
||||
action: t("mediabrowser.labels.converting"),
|
||||
},
|
||||
},
|
||||
// });
|
||||
}));
|
||||
}
|
||||
|
||||
function handleOnProgress(uri, percent, loaded) {
|
||||
setProgress((progress) => ({
|
||||
...progress,
|
||||
speed: loaded - progress.files[uri].loaded,
|
||||
action:
|
||||
percent === 1
|
||||
? t("mediabrowser.labels.converting")
|
||||
: t("mediabrowser.labels.uploading"),
|
||||
files: {
|
||||
...progress.files,
|
||||
[uri]: {
|
||||
...progress.files[uri],
|
||||
percent,
|
||||
action:
|
||||
percent === 1
|
||||
? t("mediabrowser.labels.converting")
|
||||
: t("mediabrowser.labels.uploading"),
|
||||
loaded: loaded,
|
||||
[asset.uri]: {
|
||||
...progress.files[asset.uri],
|
||||
uploadEnd: new Date(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
function handleOnError(...props) {
|
||||
logImEXEvent("imexmobile_upload_documents_error", { props });
|
||||
}
|
||||
|
||||
const onDone = async (data) => {
|
||||
//Validate to make sure the totals for the file sizes do not exceed the total on the job.
|
||||
setProgress({
|
||||
files: _.keyBy(data, "id"),
|
||||
loading: true,
|
||||
uploadInProgress: true,
|
||||
function handleOnProgress({ uri, filename }, percent, loaded) {
|
||||
//NEED REDO
|
||||
setProgress((progress) => {
|
||||
return {
|
||||
...progress,
|
||||
totalUploaded:
|
||||
progress.totalUploaded +
|
||||
(loaded - (progress.files[uri]?.loaded || 0)),
|
||||
files: {
|
||||
...progress.files,
|
||||
[uri]: {
|
||||
...progress.files[uri],
|
||||
percent,
|
||||
filename,
|
||||
speed: loaded - (progress.files[uri]?.loaded || 0),
|
||||
loaded: loaded,
|
||||
uploadStart: progress.files[uri]?.uploadStart || new Date(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
function handleOnError(error) {
|
||||
logImEXEvent("imexmobile_upload_documents_error", { error });
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Unable to upload document.",
|
||||
text2: error,
|
||||
autoHide: false,
|
||||
});
|
||||
}
|
||||
|
||||
const onDone = async (selectedFiles) => {
|
||||
setProgress((progress) => {
|
||||
return {
|
||||
...progress,
|
||||
uploadInProgress: true,
|
||||
statusText: "Preparing upload...",
|
||||
};
|
||||
});
|
||||
|
||||
//Validate to make sure the totals for the file sizes do not exceed the total on the job.
|
||||
const data = [];
|
||||
const totalOfUploads = await selectedFiles.reduce(async (acc, val) => {
|
||||
//Get the size of the file based on URI.
|
||||
const info = await FileSystem.getInfoAsync(val.uri, { size: true });
|
||||
data.push({ ...info, ...val }); //Add in the size.
|
||||
val.albumId && MediaLibrary.migrateAlbumIfNeededAsync(val.albumId);
|
||||
return (await acc) + info.size;
|
||||
}, 0);
|
||||
|
||||
if (selectedCameraJobId !== "temp") {
|
||||
const queryData = await client.query({
|
||||
@@ -124,11 +146,6 @@ export function UploadProgress({
|
||||
jobId: selectedCameraJobId,
|
||||
},
|
||||
});
|
||||
const totalOfUploads = await data.reduce(async (acc, val) => {
|
||||
//Get the size of the file based on URI.
|
||||
const info = await FileSystem.getInfoAsync(val.uri, { size: true });
|
||||
return (await acc) + info.size;
|
||||
}, 0);
|
||||
|
||||
if (
|
||||
bodyshop.jobsizelimit -
|
||||
@@ -140,7 +157,7 @@ export function UploadProgress({
|
||||
...progress,
|
||||
speed: 0,
|
||||
action: null,
|
||||
loading: false,
|
||||
statusText: null,
|
||||
uploadInProgress: false,
|
||||
}));
|
||||
Alert.alert(
|
||||
@@ -150,10 +167,22 @@ export function UploadProgress({
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Sequentially await the proms.
|
||||
//We made it this far. We have enough space, so let's start uploading.
|
||||
setProgress((progress) => ({
|
||||
...progress,
|
||||
totalToUpload: totalOfUploads,
|
||||
totalUploaded: 0,
|
||||
totalFilesCompleted: 0,
|
||||
startTime: new Date(),
|
||||
totalFiles: data.length,
|
||||
currentFile: null,
|
||||
statusText: null,
|
||||
files: {}, //uri is the key, value is progress
|
||||
}));
|
||||
|
||||
for (var i = 0; i < data.length + 4; i = i + 4) {
|
||||
//Reset the files.
|
||||
setProgress((progress) => ({ ...progress, files: {} }));
|
||||
let proms = [];
|
||||
if (data[i]) {
|
||||
proms.push(CreateUploadProm(data[i]));
|
||||
@@ -171,38 +200,57 @@ export function UploadProgress({
|
||||
await Promise.all(proms);
|
||||
}
|
||||
|
||||
//Everything is uploaded, delete the succesful ones.
|
||||
if (deleteAfterUpload) {
|
||||
try {
|
||||
await MediaLibrary.deleteAssetsAsync(filesToDelete);
|
||||
console.log("Trying to Delete", filesToDelete);
|
||||
if (Platform.OS === "android") {
|
||||
await Promise.all(
|
||||
filesToDelete.map(async (f) =>
|
||||
MediaLibrary.removeAssetsFromAlbumAsync(f, f.albumId)
|
||||
)
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
"Delete Result",
|
||||
await MediaLibrary.deleteAssetsAsync(filesToDelete.map((f) => f.id))
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Unable to delete picture.", error);
|
||||
Sentry.Native.captureException(error);
|
||||
}
|
||||
}
|
||||
filesToDelete = [];
|
||||
setProgress({
|
||||
loading: false,
|
||||
speed: 0,
|
||||
action: null,
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: ` Upload completed.`,
|
||||
//
|
||||
// text2: duration,
|
||||
});
|
||||
//Reset state.
|
||||
|
||||
setProgress({
|
||||
uploadInProgress: false,
|
||||
files: {}, //uri is the key, value is progress
|
||||
totalToUpload: 0,
|
||||
totalUploaded: 0,
|
||||
totalFilesCompleted: 0,
|
||||
startTime: null,
|
||||
totalFiles: 0,
|
||||
currentFile: null,
|
||||
files: {},
|
||||
});
|
||||
|
||||
forceRerender();
|
||||
};
|
||||
|
||||
const CreateUploadProm = async (p) => {
|
||||
let filename;
|
||||
filename = p.filename || p.uri.split("/").pop();
|
||||
|
||||
await handleUpload(
|
||||
return handleUpload(
|
||||
{
|
||||
filename,
|
||||
mediaId: p.id,
|
||||
onError: handleOnError,
|
||||
onProgress: ({ percent, loaded }) =>
|
||||
handleOnProgress(p.id, percent, loaded),
|
||||
onSuccess: () => handleOnSuccess(p.id),
|
||||
handleOnProgress(p, percent, loaded),
|
||||
onSuccess: () => handleOnSuccess(p),
|
||||
},
|
||||
{
|
||||
bodyshop: bodyshop,
|
||||
@@ -211,20 +259,6 @@ export function UploadProgress({
|
||||
photo: p,
|
||||
}
|
||||
);
|
||||
|
||||
//Set the state to mark that it's done.
|
||||
setProgress((progress) => ({
|
||||
...progress,
|
||||
action: null,
|
||||
speed: 0,
|
||||
files: {
|
||||
...progress.files,
|
||||
[p.id]: {
|
||||
...progress.files[p.id],
|
||||
action: null,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -233,20 +267,31 @@ export function UploadProgress({
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
onRequestClose={() => {
|
||||
Alert.alert("Modal has been closed.");
|
||||
Alert.alert("Cancel?", "Do you want to abort the upload?", [
|
||||
{
|
||||
text: "Yes",
|
||||
onPress: () => {
|
||||
setUploads(null);
|
||||
setProgress({
|
||||
uploadInProgress: false,
|
||||
totalToUpload: 0,
|
||||
totalUploaded: 0,
|
||||
totalFilesCompleted: 0,
|
||||
startTime: null,
|
||||
totalFiles: 0,
|
||||
currentFile: null,
|
||||
files: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
{ text: "No" },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<View style={styles.modal}>
|
||||
{progress.loading && <ActivityIndicator />}
|
||||
{progress.action && (
|
||||
<Text>{`${progress.action} ${
|
||||
(progress.speed !== 0 || !progress.speed) &&
|
||||
`- ${formatBytes(progress.speed)}/sec`
|
||||
}`}</Text>
|
||||
)}
|
||||
<ScrollView contentContainerStyle={styles.centeredView}>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modal}>
|
||||
{Object.keys(progress.files).map((key) => (
|
||||
<View key={progress.files[key].id} style={styles.progressItem}>
|
||||
<View key={key} style={styles.progressItem}>
|
||||
<Text style={styles.progressText}>
|
||||
{progress.files[key].filename}
|
||||
</Text>
|
||||
@@ -256,19 +301,59 @@ export function UploadProgress({
|
||||
style={styles.progress}
|
||||
color={progress.files[key].percent === 1 ? "green" : "blue"}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text>{`${formatBytes(
|
||||
progress.files[key].loaded /
|
||||
(((progress.files[key].uploadEnd || new Date()) -
|
||||
progress.files[key].uploadStart) /
|
||||
1000)
|
||||
)}/sec`}</Text>
|
||||
{progress.files[key].percent === 1 && (
|
||||
<>
|
||||
<ActivityIndicator style={{ marginLeft: 12 }} />
|
||||
<Text style={{ marginLeft: 4 }}>Processing...</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
<View style={styles.centeredView}>
|
||||
{progress.statusText ? (
|
||||
<>
|
||||
<ActivityIndicator style={{ marginLeft: 12 }} />
|
||||
<Text style={{ marginLeft: 4 }}>{progress.statusText}</Text>
|
||||
<Divider />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>{`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}</Text>
|
||||
<Text>{`${formatBytes(progress.totalUploaded)} of ${formatBytes(
|
||||
progress.totalToUpload
|
||||
)} uploaded.`}</Text>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
modal: {
|
||||
modalContainer: {
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
marginTop: 50,
|
||||
marginBottom: 60,
|
||||
justifyContent: "center",
|
||||
},
|
||||
modal: {
|
||||
//flex: 1,
|
||||
display: "flex",
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
backgroundColor: "white",
|
||||
@@ -284,9 +369,8 @@ const styles = StyleSheet.create({
|
||||
elevation: 5,
|
||||
},
|
||||
centeredView: {
|
||||
flex: 1,
|
||||
// justifyContent: "center",
|
||||
// alignItems: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginTop: 22,
|
||||
},
|
||||
progressItem: {
|
||||
|
||||
22
eas.json
Normal file
22
eas.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 0.52.0"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"test": {
|
||||
"releaseChannel": "test",
|
||||
"env": { "ANDROID_SDK_ROOT": "/Users/pfic/Library/Android/sdk" }
|
||||
},
|
||||
"production": {
|
||||
"releaseChannel": "production",
|
||||
"env": { "ANDROID_SDK_ROOT": "/Users/pfic/Library/Android/sdk" }
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@ import firebase from "firebase/app";
|
||||
import "firebase/auth";
|
||||
import env from "../env";
|
||||
|
||||
if (!firebase.apps.length) {
|
||||
firebase.initializeApp(env.firebase);
|
||||
}
|
||||
import { initializeApp } from "firebase/app";
|
||||
import { getAuth, initializeAuth } from "firebase/auth";
|
||||
import { getReactNativePersistence } from "firebase/auth/react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
const defaultApp = initializeApp(env.firebase);
|
||||
initializeAuth(defaultApp, {
|
||||
persistence: getReactNativePersistence(AsyncStorage),
|
||||
});
|
||||
export const auth = getAuth();
|
||||
|
||||
export const auth = firebase.auth();
|
||||
//export const analytics = firebase.analytics();
|
||||
|
||||
export default firebase;
|
||||
@@ -19,14 +25,3 @@ export const getCurrentUser = () => {
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
export const updateCurrentUser = (userDetails) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
|
||||
userAuth.updateProfile(userDetails).then((r) => {
|
||||
unsubscribe();
|
||||
resolve(userAuth);
|
||||
});
|
||||
}, reject);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,11 @@ export const QUERY_BODYSHOP = gql`
|
||||
id
|
||||
jobsizelimit
|
||||
md_ro_statuses
|
||||
md_order_statuses
|
||||
uselocalmediaserver
|
||||
localmediaserverhttp
|
||||
shopname
|
||||
messagingservicesid
|
||||
md_referral_sources
|
||||
md_messaging_presets
|
||||
md_parts_locations
|
||||
md_notes_presets
|
||||
features
|
||||
localmediatoken
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -71,9 +71,8 @@ const link = split(
|
||||
// "##Intercepted GQL Transaction : " +
|
||||
// definition.operation +
|
||||
// "|" +
|
||||
// definition.name.value +
|
||||
// "##",
|
||||
// query
|
||||
// // definition.name.value +
|
||||
// "##"
|
||||
// );
|
||||
return (
|
||||
definition.kind === "OperationDefinition" &&
|
||||
@@ -128,11 +127,16 @@ export const client = new ApolloClient({
|
||||
//link: from([apolloLogger, errorLink, authLink, link]),
|
||||
link: from([authLink, link]),
|
||||
cache,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
|
||||
// connectToDevTools: process.env.NODE_ENV !== "production",
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
},
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -183,7 +183,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
date_exported
|
||||
status
|
||||
owner_owing
|
||||
joblines {
|
||||
joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
|
||||
id
|
||||
unq_seq
|
||||
line_ind
|
||||
|
||||
6
metro.config.js
Normal file
6
metro.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const { getDefaultConfig } = require("metro-config");
|
||||
const { resolver: defaultResolver } = getDefaultConfig.getDefaultValues();
|
||||
exports.resolver = {
|
||||
...defaultResolver,
|
||||
sourceExts: [...defaultResolver.sourceExts, "cjs", "jsx"],
|
||||
};
|
||||
119
package.json
119
package.json
@@ -8,69 +8,86 @@
|
||||
"eject": "expo eject",
|
||||
"release:test": "expo publish --release-channel test",
|
||||
"release:production": "expo publish --release-channel production",
|
||||
"build:ios:production": "expo build:ios --release-channel production",
|
||||
"build:ios:test": "expo build:ios --release-channel test",
|
||||
"build:android:production": "expo build:android --release-channel production",
|
||||
"build:android:test": "expo build:android --release-channel test"
|
||||
"build:production": "eas build --profile production",
|
||||
"build:test": "eas build --profile test",
|
||||
"build:test:local:ios": "eas build --profile test --platform ios --local",
|
||||
"build:test:local:android": "eas build --profile test --platform android --local",
|
||||
"build:production:local:ios": "eas build --profile production --platform ios --local",
|
||||
"build:production:local:android": "eas build --profile production --platform android --local"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.19",
|
||||
"@expo/vector-icons": "^12.0.0",
|
||||
"@react-native-async-storage/async-storage": "^1.13.0",
|
||||
"@apollo/client": "^3.7.0-alpha.3",
|
||||
"@expo/vector-icons": "^13.0.0",
|
||||
"@react-native-async-storage/async-storage": "~1.17.6",
|
||||
"@react-native-community/art": "^1.2.0",
|
||||
"@react-native-community/masked-view": "0.1.10",
|
||||
"@react-navigation/bottom-tabs": "^5.11.11",
|
||||
"@react-navigation/drawer": "^5.12.5",
|
||||
"@react-navigation/native": "^5.9.4",
|
||||
"@react-navigation/stack": "^5.14.5",
|
||||
"axios": "^0.21.0",
|
||||
"cloudinary-core": "^2.11.4",
|
||||
"dinero.js": "^1.8.1",
|
||||
"expo": "^41.0.0",
|
||||
"expo-app-loading": "^1.0.3",
|
||||
"expo-av": "~9.1.2",
|
||||
"expo-camera": "~11.0.2",
|
||||
"expo-file-system": "~11.0.2",
|
||||
"expo-firebase-analytics": "~4.0.2",
|
||||
"expo-font": "~9.1.0",
|
||||
"expo-images-picker": "git+https://github.com/snaptsoft/expo-images-picker/",
|
||||
"expo-localization": "~10.1.0",
|
||||
"expo-media-library": "~12.0.2",
|
||||
"expo-permissions": "~12.0.1",
|
||||
"expo-status-bar": "~1.0.4",
|
||||
"expo-video-thumbnails": "~5.1.0",
|
||||
"firebase": "8.2.3",
|
||||
"formik": "^2.2.8",
|
||||
"@react-native-community/cli-debugger-ui": "^7.0.3",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-navigation/bottom-tabs": "^6.2.0",
|
||||
"@react-navigation/drawer": "^6.3.1",
|
||||
"@react-navigation/native": "^6.0.8",
|
||||
"@react-navigation/stack": "^6.1.1",
|
||||
"axios": "^0.27.2",
|
||||
"cloudinary-core": "^2.12.3",
|
||||
"dinero.js": "^1.9.1",
|
||||
"expo": "^45.0.5",
|
||||
"expo-app-loading": "~2.0.0",
|
||||
"expo-application": "~4.1.0",
|
||||
"expo-av": "~11.2.3",
|
||||
"expo-camera": "~12.2.0",
|
||||
"expo-constants": "~13.1.1",
|
||||
"expo-dev-client": "~1.0.0",
|
||||
"expo-device": "~4.2.0",
|
||||
"expo-file-system": "~14.0.0",
|
||||
"expo-firebase-analytics": "~7.0.0",
|
||||
"expo-font": "~10.1.0",
|
||||
"expo-image-manipulator": "~10.3.1",
|
||||
"expo-images-picker": "^2.4.1",
|
||||
"expo-localization": "~13.0.0",
|
||||
"expo-media-library": "~14.1.0",
|
||||
"expo-permissions": "~13.2.0",
|
||||
"expo-status-bar": "~1.3.0",
|
||||
"expo-system-ui": "~1.2.0",
|
||||
"expo-updates": "~0.13.2",
|
||||
"expo-video-thumbnails": "~6.3.0",
|
||||
"firebase": "^9.8.3",
|
||||
"formik": "^2.2.9",
|
||||
"graphql": "^15.4.0",
|
||||
"i18next": "^20.3.1",
|
||||
"i18next": "^21.8.10",
|
||||
"intl": "^1.2.5",
|
||||
"lodash": "^4.17.20",
|
||||
"luxon": "^1.27.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-i18next": "^11.10.0",
|
||||
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
|
||||
"react-native-gesture-handler": "~1.10.2",
|
||||
"react-native-image-gallery": "archriss/react-native-image-gallery#152/head",
|
||||
"luxon": "^2.3.1",
|
||||
"mime": "^3.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"normalize-url": "^7.0.3",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-i18next": "^11.17.2",
|
||||
"react-native": "0.68.2",
|
||||
"react-native-gesture-handler": "~2.2.1",
|
||||
"react-native-image-gallery": "^2.1.5",
|
||||
"react-native-image-viewing": "^0.2.2",
|
||||
"react-native-indicators": "^0.17.0",
|
||||
"react-native-pager-view": "5.0.12",
|
||||
"react-native-paper": "^4.9.1",
|
||||
"react-native-progress": "^4.1.2",
|
||||
"react-native-reanimated": "~2.1.0",
|
||||
"react-native-screens": "~3.0.0",
|
||||
"react-native-tab-view": "3.0.1",
|
||||
"react-native-web": "~0.13.12",
|
||||
"react-redux": "^7.2.4",
|
||||
"redux": "^4.1.0",
|
||||
"react-native-pager-view": "5.4.15",
|
||||
"react-native-paper": "^4.11.2",
|
||||
"react-native-progress": "^5.0.0",
|
||||
"react-native-reanimated": "~2.8.0",
|
||||
"react-native-safe-area-context": "4.2.4",
|
||||
"react-native-screens": "~3.11.1",
|
||||
"react-native-tab-view": "3.1.1",
|
||||
"react-native-toast-message": "^2.1.5",
|
||||
"react-native-web": "0.17.7",
|
||||
"react-redux": "^7.2.6",
|
||||
"redux": "^4.1.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.1.3",
|
||||
"reselect": "^4.0.0",
|
||||
"sentry-expo": "^3.1.0",
|
||||
"reselect": "^4.1.6",
|
||||
"sentry-expo": "^4.2.0",
|
||||
"subscriptions-transport-ws": "^0.9.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.9.0",
|
||||
"babel-preset-expo": "8.3.0",
|
||||
"@babel/core": "^7.12.9",
|
||||
"babel-preset-expo": "~9.1.0",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-native": "^3.11.0"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as Analytics from "expo-firebase-analytics";
|
||||
import { signInWithEmailAndPassword, signOut } from "firebase/auth";
|
||||
import { all, call, put, takeLatest } from "redux-saga/effects";
|
||||
import * as Sentry from "sentry-expo";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
import {
|
||||
auth,
|
||||
getCurrentUser,
|
||||
updateCurrentUser,
|
||||
} from "../../firebase/firebase.utils";
|
||||
import { logImEXEvent } from "../../firebase/firebase.analytics";
|
||||
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
|
||||
import { client } from "../../graphql/client";
|
||||
import {
|
||||
@@ -29,7 +31,7 @@ export function* onEmailSignInStart() {
|
||||
export function* signInWithEmail({ payload: { email, password } }) {
|
||||
try {
|
||||
logImEXEvent("imexmobile_sign_in_attempt", { user: email });
|
||||
const { user } = yield auth.signInWithEmailAndPassword(email, password);
|
||||
const { user } = yield signInWithEmailAndPassword(auth, email, password);
|
||||
yield put(
|
||||
signInSuccess({
|
||||
uid: user.uid,
|
||||
@@ -41,6 +43,7 @@ export function* signInWithEmail({ payload: { email, password } }) {
|
||||
);
|
||||
} catch (error) {
|
||||
yield put(signInFailure(error));
|
||||
|
||||
//logImEXEvent("redux_sign_in_failure", { user: email, error });
|
||||
}
|
||||
}
|
||||
@@ -78,7 +81,7 @@ export function* signOutStart() {
|
||||
try {
|
||||
logImEXEvent("imexmobile_sign_out");
|
||||
|
||||
yield auth.signOut();
|
||||
yield signOut(auth);
|
||||
yield put(signOutSuccess());
|
||||
} catch (error) {
|
||||
yield put(signOutFailure(error.message));
|
||||
@@ -105,12 +108,21 @@ export function* onSignInSuccess() {
|
||||
export function* signInSuccessSaga({ payload }) {
|
||||
try {
|
||||
Analytics.setUserId(payload.email);
|
||||
Sentry.Native.setUser({ email: payload.email });
|
||||
|
||||
const shop = yield client.query({ query: QUERY_BODYSHOP });
|
||||
logImEXEvent("imexmobile_sign_in_success", payload);
|
||||
yield put(setBodyshop(shop.data.bodyshops[0]));
|
||||
// yield put(
|
||||
// setBodyshop({
|
||||
// ...shop.data.bodyshops[0],
|
||||
// uselocalmediaserver: true,
|
||||
// localmediaserverhttp: `http://192.168.1.235:8000`,
|
||||
// })
|
||||
// );
|
||||
} catch (error) {
|
||||
console.log("UH-OH. Couldn't get shop details.", error);
|
||||
Sentry.Native.captureException(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"app": {
|
||||
"nomobileaccess": "Your shop does not currently have access to ImEX Mobile. ",
|
||||
"title": "ImEX Mobile"
|
||||
},
|
||||
"camera": {
|
||||
@@ -77,7 +78,8 @@
|
||||
"labels": {
|
||||
"activejobs": "Jobs",
|
||||
"detail": "Job Detail",
|
||||
"nojobs": "There are no active jobs."
|
||||
"nojobs": "There are no active jobs.",
|
||||
"search": "Search..."
|
||||
},
|
||||
"titles": {
|
||||
"jobtab": "Jobs"
|
||||
@@ -91,6 +93,7 @@
|
||||
"labels": {
|
||||
"converting": "Converting",
|
||||
"deleteafterupload": "Delete After Upload",
|
||||
"localserver": "Local Server URL: {{url}}",
|
||||
"nomedia": "Look's like there's no media on your device. Take some photos or videos and they will appear here.",
|
||||
"selectjob": "--- Select a job ---",
|
||||
"selectjobassetselector": "Please select a job to upload media. ",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"app": {
|
||||
"nomobileaccess": "",
|
||||
"title": ""
|
||||
},
|
||||
"camera": {
|
||||
@@ -77,7 +78,8 @@
|
||||
"labels": {
|
||||
"activejobs": "",
|
||||
"detail": "",
|
||||
"nojobs": ""
|
||||
"nojobs": "",
|
||||
"search": ""
|
||||
},
|
||||
"titles": {
|
||||
"jobtab": ""
|
||||
@@ -91,6 +93,7 @@
|
||||
"labels": {
|
||||
"converting": "",
|
||||
"deleteafterupload": "",
|
||||
"localserver": "",
|
||||
"nomedia": "",
|
||||
"selectjob": "",
|
||||
"selectjobassetselector": "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"translation": {
|
||||
"app": {
|
||||
"nomobileaccess": "",
|
||||
"title": ""
|
||||
},
|
||||
"camera": {
|
||||
@@ -77,7 +78,8 @@
|
||||
"labels": {
|
||||
"activejobs": "",
|
||||
"detail": "",
|
||||
"nojobs": ""
|
||||
"nojobs": "",
|
||||
"search": ""
|
||||
},
|
||||
"titles": {
|
||||
"jobtab": ""
|
||||
@@ -91,6 +93,7 @@
|
||||
"labels": {
|
||||
"converting": "",
|
||||
"deleteafterupload": "",
|
||||
"localserver": "",
|
||||
"nomedia": "",
|
||||
"selectjob": "",
|
||||
"selectjobassetselector": "",
|
||||
|
||||
@@ -5,6 +5,7 @@ import { INSERT_NEW_DOCUMENT } from "../graphql/documents.queries";
|
||||
import { axiosAuthInterceptorId } from "./CleanAxios";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import { gql } from "@apollo/client";
|
||||
import * as Sentry from "sentry-expo";
|
||||
|
||||
//Context: currentUserEmail, bodyshop, jobid, invoiceid
|
||||
|
||||
@@ -13,24 +14,25 @@ var cleanAxios = axios.create();
|
||||
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
|
||||
|
||||
export const handleUpload = async (ev, context) => {
|
||||
const { filename, mediaId, onError, onSuccess, onProgress } = ev;
|
||||
const { mediaId, onError, onSuccess, onProgress } = ev;
|
||||
const { bodyshop, jobId } = context;
|
||||
|
||||
const imageData = await MediaLibrary.getAssetInfoAsync(mediaId);
|
||||
const newFile = await (await fetch(imageData.localUri)).blob();
|
||||
const newFile = await (
|
||||
await fetch(imageData.localUri || imageData.uri)
|
||||
).blob();
|
||||
let extension = imageData.localUri.split(".").pop();
|
||||
let key = `${bodyshop.id}/${jobId}/${(filename || newFile.data.name).replace(
|
||||
/\.[^/.]+$/,
|
||||
""
|
||||
)}-${new Date().getTime()}`;
|
||||
let key = `${bodyshop.id}/${jobId}/${(
|
||||
imageData.filename || imageData.uri.split("/").pop()
|
||||
).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`;
|
||||
|
||||
const res = await uploadToCloudinary(
|
||||
key,
|
||||
mediaId,
|
||||
imageData,
|
||||
extension,
|
||||
newFile.type,
|
||||
newFile,
|
||||
newFile.type, //Filetype
|
||||
newFile, //File
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
@@ -51,41 +53,33 @@ export const uploadToCloudinary = async (
|
||||
onProgress,
|
||||
context
|
||||
) => {
|
||||
const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray, photo } =
|
||||
context;
|
||||
const { bodyshop, jobId, uploaded_by } = 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;
|
||||
|
||||
const upload_preset = fileType.startsWith("video")
|
||||
? "incoming_upload_video"
|
||||
: "incoming_upload";
|
||||
|
||||
//Get the signed url.
|
||||
|
||||
let signedURLResponse;
|
||||
try {
|
||||
signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, {
|
||||
public_id: public_id,
|
||||
tags: tags,
|
||||
timestamp: timestamp,
|
||||
|
||||
upload_preset: upload_preset,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("ERROR GETTING SIGNED URL", error);
|
||||
Sentry.Native.captureException(error);
|
||||
|
||||
return { success: false, error: error };
|
||||
}
|
||||
|
||||
if (signedURLResponse.status !== 200) {
|
||||
console.log("Error Getting Signed URL", signedURLResponse.statusText);
|
||||
if (onError) onError(signedURLResponse.statusText);
|
||||
|
||||
return { success: false, error: signedURLResponse.statusText };
|
||||
}
|
||||
|
||||
@@ -93,7 +87,10 @@ export const uploadToCloudinary = async (
|
||||
|
||||
var signature = signedURLResponse.data;
|
||||
var options = {
|
||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||
headers: {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: (e) => {
|
||||
if (onProgress)
|
||||
onProgress({ percent: e.loaded / e.total, loaded: e.loaded });
|
||||
@@ -109,7 +106,6 @@ export const uploadToCloudinary = async (
|
||||
formData.append("upload_preset", upload_preset);
|
||||
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);
|
||||
|
||||
@@ -121,13 +117,13 @@ export const uploadToCloudinary = async (
|
||||
fileType
|
||||
)}/upload`,
|
||||
formData,
|
||||
{
|
||||
...options,
|
||||
}
|
||||
options
|
||||
);
|
||||
// console.log("Cloudinary Upload Response", cloudinaryUploadResponse.data);
|
||||
} catch (error) {
|
||||
console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse);
|
||||
Sentry.Native.captureException(error);
|
||||
|
||||
if (onError) onError(error.message);
|
||||
return { success: false, error: error };
|
||||
}
|
||||
|
||||
@@ -144,35 +140,10 @@ export const uploadToCloudinary = async (
|
||||
//Insert the document with the matching key.
|
||||
const documentInsert = await client.mutate({
|
||||
mutation: INSERT_NEW_DOCUMENT,
|
||||
|
||||
update: (cache, { data }) => {
|
||||
cache.modify({
|
||||
fields: {
|
||||
documents: (existingDocs = []) => {
|
||||
const newDocRef = cache.writeFragment({
|
||||
data: data.insert_documents.returning[0],
|
||||
fragment: gql`
|
||||
fragment newDoc on documents {
|
||||
id
|
||||
name
|
||||
key
|
||||
type
|
||||
takenat
|
||||
extension
|
||||
jobid
|
||||
}
|
||||
`,
|
||||
});
|
||||
return [...existingDocs, newDocRef];
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
variables: {
|
||||
docInput: [
|
||||
{
|
||||
...(jobId ? { jobid: jobId } : {}),
|
||||
...(billId ? { billid: billId } : {}),
|
||||
uploaded_by: uploaded_by,
|
||||
key: key,
|
||||
type: fileType,
|
||||
@@ -194,19 +165,8 @@ export const uploadToCloudinary = async (
|
||||
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 {
|
||||
success: false,
|
||||
error: JSON.stringify(documentInsert.errors),
|
||||
@@ -231,6 +191,7 @@ export function formatBytes(a, b = 2) {
|
||||
if (0 === a || !a) return "0 Bytes";
|
||||
const c = 0 > b ? 0 : b,
|
||||
d = Math.floor(Math.log(a) / Math.log(1024));
|
||||
|
||||
return (
|
||||
parseFloat((a / Math.pow(1024, d)).toFixed(c)) +
|
||||
" " +
|
||||
|
||||
109
util/local-document-upload.utility.js
Normal file
109
util/local-document-upload.utility.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import axios from "axios";
|
||||
import { store } from "../redux/store";
|
||||
import mime from "mime";
|
||||
import * as MediaLibrary from "expo-media-library";
|
||||
import * as Sentry from "sentry-expo";
|
||||
|
||||
axios.interceptors.request.use(
|
||||
function (config) {
|
||||
config.metadata = { startTime: new Date() };
|
||||
return config;
|
||||
},
|
||||
function (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
response.config.metadata.endTime = new Date();
|
||||
response.duration =
|
||||
response.config.metadata.endTime - response.config.metadata.startTime;
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
error.config.metadata.endTime = new Date();
|
||||
error.duration =
|
||||
error.config.metadata.endTime - error.config.metadata.startTime;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const handleLocalUpload = async ({
|
||||
files,
|
||||
onError,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
context,
|
||||
}) => {
|
||||
const { jobid } = context;
|
||||
const bodyshop = store.getState().user.bodyshop;
|
||||
try {
|
||||
var options = {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
ims_token: bodyshop.localmediatoken,
|
||||
},
|
||||
onUploadProgress: (e) => {
|
||||
if (onProgress)
|
||||
onProgress({ percent: e.loaded / e.total, loaded: e.loaded });
|
||||
},
|
||||
};
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("jobid", jobid);
|
||||
|
||||
const filesList = [];
|
||||
for (const file of files) {
|
||||
const imageData = await MediaLibrary.getAssetInfoAsync(file.id);
|
||||
const mimeType = mime.getType(imageData.uri);
|
||||
filesList.push({
|
||||
uri: imageData.localUri || imageData.uri,
|
||||
type: mimeType,
|
||||
name: imageData.filename,
|
||||
});
|
||||
formData.append("file", {
|
||||
uri: imageData.localUri || imageData.uri,
|
||||
type: mimeType,
|
||||
name: imageData.filename,
|
||||
});
|
||||
}
|
||||
|
||||
//formData.append("file", files);
|
||||
formData.append("skip_thumbnail", true);
|
||||
|
||||
try {
|
||||
const imexMediaServerResponse = await axios.post(
|
||||
`${bodyshop.localmediaserverhttp}/jobs/upload`,
|
||||
formData,
|
||||
options
|
||||
);
|
||||
|
||||
if (imexMediaServerResponse.status !== 200) {
|
||||
if (onError) {
|
||||
onError({
|
||||
error:
|
||||
imexMediaServerResponse.data ||
|
||||
imexMediaServerResponse.statusText,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onSuccess &&
|
||||
onSuccess({
|
||||
duration: imexMediaServerResponse.headers["x-response-time"],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
Sentry.Native.captureException(error);
|
||||
|
||||
console.log("Error uploading documents:", error.message);
|
||||
onError && onError({ error: error.message });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Uncaught error", error);
|
||||
Sentry.Native.captureException(error);
|
||||
|
||||
onError && onError({ error: error.message });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user