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:
Patrick Fic
2022-08-17 14:58:16 -07:00
32 changed files with 6603 additions and 5396 deletions

4
.gitignore vendored
View File

@@ -12,3 +12,7 @@ yarn-error.log
# macOS # macOS
.DS_Store .DS_Store
*.ipa
*.aab

38
App.js
View File

@@ -8,12 +8,23 @@ import ScreenMainComponent from "./components/screen-main/screen-main.component"
import { logImEXEvent } from "./firebase/firebase.analytics"; import { logImEXEvent } from "./firebase/firebase.analytics";
import { client } from "./graphql/client"; import { client } from "./graphql/client";
import { persistor, store } from "./redux/store"; import { persistor, store } from "./redux/store";
import "intl";
import "intl/locale-data/jsonp/en";
import "./translations/i18n"; import "./translations/i18n";
import "expo-asset";
import Toast from "react-native-toast-message";
import { SafeAreaProvider } from "react-native-safe-area-context";
Sentry.init({ Sentry.init({
dsn: "https://8d6c3de1940a4e4f8b81cf4d2150bdea@o492140.ingest.sentry.io/5558869", dsn: "https://8d6c3de1940a4e4f8b81cf4d2150bdea@o492140.ingest.sentry.io/5558869",
enableInExpoDevelopment: true, 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(); Sentry.Native.nativeCrash();
@@ -22,7 +33,7 @@ const theme = {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
...DefaultTheme.colors, ...DefaultTheme.colors,
primary: "dodgerblue", primary: "#1890ff",
accent: "tomato", accent: "tomato",
}, },
}; };
@@ -34,15 +45,18 @@ export default class App extends React.Component {
render() { render() {
return ( return (
<Provider store={store}> <SafeAreaProvider>
<PersistGate persistor={persistor}> <Provider store={store}>
<ApolloProvider client={client}> <PersistGate persistor={persistor}>
<PaperProvider theme={theme}> <ApolloProvider client={client}>
<ScreenMainComponent /> <PaperProvider theme={theme}>
</PaperProvider> <ScreenMainComponent />
</ApolloProvider> <Toast />
</PersistGate> </PaperProvider>
</Provider> </ApolloProvider>
</PersistGate>
</Provider>
</SafeAreaProvider>
); );
} }
} }

View File

@@ -2,24 +2,25 @@
"expo": { "expo": {
"name": "ImEX Mobile", "name": "ImEX Mobile",
"slug": "imexmobile", "slug": "imexmobile",
"version": "1.2.3", "version": "1.3.7",
"extra": { "expover": "1" }, "extra": {
"expover": "6"
},
"orientation": "default", "orientation": "default",
"icon": "./assets/logo192noa.png", "icon": "./assets/logo192noa.png",
"ios": { "ios": {
"supportsTablet": true, "supportsTablet": true,
"bundleIdentifier": "com.imex.imexmobile", "bundleIdentifier": "com.imex.imexmobile",
"buildNumber": "1.2.3", "buildNumber": "6",
"googleServicesFile": "./GoogleService-Info.plist" "googleServicesFile": "./GoogleService-Info.plist"
}, },
"android": { "android": {
"package": "com.imex.imexmobile", "package": "com.imex.imexmobile",
"versionCode": 1020300, "versionCode": 1100019,
"googleServicesFile": "./google-services.json" "googleServicesFile": "./google-services.json"
}, },
"splash": { "splash": {
"image": "./assets/Splash.png", "image": "./assets/Splash.png",
"backgroundColor": "#efefef" "backgroundColor": "#efefef"
}, },
"notification": { "notification": {
@@ -29,7 +30,6 @@
"fallbackToCacheTimeout": 0 "fallbackToCacheTimeout": 0
}, },
"assetBundlePatterns": ["**/*"], "assetBundlePatterns": ["**/*"],
"web": { "web": {
"favicon": "./assets/logo192noa.png", "favicon": "./assets/logo192noa.png",
"config": { "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"
}
]
]
} }
} }

View File

@@ -24,6 +24,27 @@
<folder_node> <folder_node>
<name>app</name> <name>app</name>
<children> <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> <concept_node>
<name>title</name> <name>title</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -1216,6 +1237,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> </children>
</folder_node> </folder_node>
<folder_node> <folder_node>
@@ -1341,6 +1383,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </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> <concept_node>
<name>nomedia</name> <name>nomedia</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -20,14 +20,24 @@ export default function JobDocumentsComponent({ job, loading, refetch }) {
const fullphotos = useMemo( const fullphotos = useMemo(
() => () =>
job.documents.map((doc) => { job.documents.map((doc, idx) => {
return { return {
id: idx,
videoUrl: videoUrl:
DetermineFileType(doc.type) === "video" && GenerateSrcUrl(doc), DetermineFileType(doc.type) === "video" && GenerateSrcUrl(doc),
source: source:
DetermineFileType(doc.type) === "video" DetermineFileType(doc.type) === "video"
? { uri: GenerateThumbUrl(doc) } ? { uri: GenerateThumbUrl(doc) }
: { uri: GenerateSrcUrl(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] [job.documents]

View File

@@ -1,7 +1,8 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import React from "react"; import React from "react";
import { RefreshControl, View, Text } from "react-native"; import { useTranslation } from "react-i18next";
import { FlatList } from "react-native-gesture-handler"; import { FlatList, RefreshControl, Text, View } from "react-native";
import { Button, Searchbar, Title } from "react-native-paper";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries"; 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 ErrorDisplay from "../error-display/error-display.component";
import JobListItem from "../job-list-item/job-list-item.component"; import JobListItem from "../job-list-item/job-list-item.component";
import LoadingDisplay from "../loading-display/loading-display.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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -23,6 +23,7 @@ export function JobListComponent({ bodyshop }) {
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"], statuses: bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
}, },
skip: !bodyshop, skip: !bodyshop,
notifyOnNetworkStatusChange: true,
}); });
const onRefresh = async () => { const onRefresh = async () => {
@@ -32,7 +33,6 @@ export function JobListComponent({ bodyshop }) {
if (loading) return <LoadingDisplay />; if (loading) return <LoadingDisplay />;
if (error) return <ErrorDisplay errorMessage={error.message} />; if (error) return <ErrorDisplay errorMessage={error.message} />;
if (data && data.jobs && data.jobs.length === 0) if (data && data.jobs && data.jobs.length === 0)
return ( return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}> <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
@@ -74,7 +74,11 @@ export function JobListComponent({ bodyshop }) {
return ( return (
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Searchbar onChangeText={onChangeSearch} value={searchQuery} /> <Searchbar
onChangeText={onChangeSearch}
value={searchQuery}
placeholder={t("joblist.labels.search")}
/>
<FlatList <FlatList
refreshControl={ refreshControl={
<RefreshControl refreshing={loading} onRefresh={onRefresh} /> <RefreshControl refreshing={loading} onRefresh={onRefresh} />

View File

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

View File

@@ -1,13 +1,7 @@
import { Ionicons } from "@expo/vector-icons"; import { SafeAreaView } from "react-native";
import { Video } from "expo-av"; import React from "react";
import React, { useState } from "react";
import { import ImageView from "react-native-image-viewing";
Dimensions,
Modal,
SafeAreaView,
TouchableOpacity,
} from "react-native";
import Gallery from "react-native-image-gallery";
export default function MediaCacheOverlay({ export default function MediaCacheOverlay({
photos, photos,
@@ -16,61 +10,73 @@ export default function MediaCacheOverlay({
imgIndex, imgIndex,
setImgIndex, setImgIndex,
}) { }) {
const [currentIndex, setcurrentIndex] = useState(0); //const videoRef = React.useRef(null);
const [dragging, setDragging] = useState(false);
const videoRef = React.useRef(null);
return ( return (
<Modal <SafeAreaView>
onDismiss={() => setPreviewVisible(false)} <ImageView
onRequestClose={() => setPreviewVisible(false)} onRequestClose={() => setPreviewVisible(false)}
visible={previewVisible} visible={previewVisible}
transparent={false} images={photos}
> imageIndex={imgIndex}
<SafeAreaView style={{ flex: 1, backgroundColor: "black" }}> onImageIndexChange={(...props) => {
<Gallery console.log(props);
initialPage={imgIndex} }}
images={photos} />
onPageScroll={({ position }) => setcurrentIndex(position)} </SafeAreaView>
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>
); );
// 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>
// );
} }

View File

@@ -10,8 +10,18 @@ import JobLines from "../job-lines/job-lines.component";
import JobNotes from "../job-notes/job-notes.component"; import JobNotes from "../job-notes/job-notes.component";
import JobTombstone from "../job-tombstone/job-tombstone.component"; import JobTombstone from "../job-tombstone/job-tombstone.component";
import LoadingDisplay from "../loading-display/loading-display.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 { const {
params: { jobId }, params: { jobId },
} = route; } = route;
@@ -45,12 +55,16 @@ export default function ScreenJobDetail({ route }) {
loading: loading, loading: loading,
refetch: refetch, refetch: refetch,
}), }),
documents: () => ...(bodyshop.uselocalmediaserver
JobDocuments({ ? {}
job: data.jobs_by_pk, : {
loading: loading, documents: () =>
refetch: refetch, JobDocuments({
}), job: data.jobs_by_pk,
loading: loading,
refetch: refetch,
}),
}),
notes: () => notes: () =>
JobNotes({ JobNotes({
job: data.jobs_by_pk, job: data.jobs_by_pk,
@@ -63,7 +77,9 @@ export default function ScreenJobDetail({ route }) {
const [routes] = React.useState([ const [routes] = React.useState([
{ key: "job", title: t("jobdetail.labels.job") }, { key: "job", title: t("jobdetail.labels.job") },
{ key: "lines", title: t("jobdetail.labels.lines") }, { 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") }, { key: "notes", title: t("jobdetail.labels.notes") },
]); ]);

View File

@@ -3,9 +3,9 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { NavigationContainer } from "@react-navigation/native"; import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack"; import { createStackNavigator } from "@react-navigation/stack";
import i18n from "i18next"; import i18n from "i18next";
import moment from "moment";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Button } from "react-native-paper"; import { Button } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics"; import { logImEXEvent } from "../../firebase/firebase.analytics";
@@ -107,6 +107,15 @@ const MoreStackNavigator = () => (
const BottomTabsNavigator = () => ( const BottomTabsNavigator = () => (
<BottomTabs.Navigator <BottomTabs.Navigator
screenOptions={({ route }) => ({ screenOptions={({ route }) => ({
// tabBarActiveTintColor: "dodgerblue",
// tabBarInactiveTintColor: "slategrey",
// tabBarStyle: [
// {
// display: "flex",
// },
// null,
// ],
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
tabBarIcon: ({ color, size }) => { tabBarIcon: ({ color, size }) => {
let iconName; let iconName;
@@ -123,24 +132,27 @@ const BottomTabsNavigator = () => (
return <Ionicons name={iconName} size={size} color={color} />; return <Ionicons name={iconName} size={size} color={color} />;
}, },
})} })}
tabBarOptions={{
activeTintColor: "dodgerblue",
inactiveTintColor: "slategrey",
}}
> >
<BottomTabs.Screen <BottomTabs.Screen
name="JobTab" name="JobTab"
options={{ title: i18n.t("joblist.titles.jobtab") }} options={{
title: i18n.t("joblist.titles.jobtab"),
headerShown: false,
}}
component={JobStackNavigator} component={JobStackNavigator}
/> />
<BottomTabs.Screen <BottomTabs.Screen
name="MediaBrowserTab" name="MediaBrowserTab"
options={{ title: i18n.t("mediabrowser.titles.mediabrowsertab") }} options={{
title: i18n.t("mediabrowser.titles.mediabrowsertab"),
headerShown: false,
}}
component={MediaBrowserStackNavigator} component={MediaBrowserStackNavigator}
/> />
<BottomTabs.Screen <BottomTabs.Screen
name="MoreTab" name="MoreTab"
options={{ title: i18n.t("more.titles.moretab") }} options={{ title: i18n.t("more.titles.moretab"), headerShown: false }}
component={MoreStackNavigator} component={MoreStackNavigator}
/> />
</BottomTabs.Navigator> </BottomTabs.Navigator>
@@ -156,24 +168,33 @@ export function ScreenMainComponent({
}, [checkUserSession]); }, [checkUserSession]);
return ( return (
<SafeAreaView style={{ flex: 1 }}> <NavigationContainer>
<NavigationContainer> {currentUser.authorized === null ? (
{currentUser.authorized === null ? ( <ScreenSplash />
<ScreenSplash /> ) : currentUser.authorized ? (
) : currentUser.authorized ? ( bodyshop ? (
bodyshop ? ( HasAccess(bodyshop) ? (
<BottomTabsNavigator /> <BottomTabsNavigator />
) : ( ) : (
<ScreenSplash /> <ScreenSplash noAccess />
) )
) : ( ) : (
<ScreenSignIn /> <ScreenSplash />
)} )
</NavigationContainer> ) : (
</SafeAreaView> <ScreenSignIn />
)}
</NavigationContainer>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ScreenMainComponent); )(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());
}

View File

@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { AssetsSelector } from "expo-images-picker"; 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 { useTranslation } from "react-i18next";
import { StyleSheet, Text, View } from "react-native"; import { StyleSheet, Text, View } from "react-native";
import { connect } from "react-redux"; 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 JobSpaceAvailable from "../job-space-available/job-space-available.component";
import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.component"; import UploadDeleteSwitch from "../upload-delete-switch/upload-delete-switch.component";
import UploadProgress from "../upload-progress/upload-progress.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({ const mapStateToProps = createStructuredSelector({
selectedCameraJobId: selectCurrentCameraJobId, selectedCameraJobId: selectCurrentCameraJobId,
bodyshop: selectBodyshop,
}); });
export function ImageBrowserScreen({ selectedCameraJobId }) { export function ImageBrowserScreen({ bodyshop, selectedCameraJobId }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [uploads, setUploads] = useState(null); const [uploads, setUploads] = useState(null);
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
@@ -29,10 +33,105 @@ export function ImageBrowserScreen({ selectedCameraJobId }) {
if (data.length !== 0) setUploads(data); 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 ( return (
<View style={[styles.flex, styles.container]}> <View style={[styles.flex, styles.container]}>
<CameraSelectJob /> <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 /> <UploadDeleteSwitch />
{!selectedCameraJobId && ( {!selectedCameraJobId && (
<View <View
@@ -49,65 +148,25 @@ export function ImageBrowserScreen({ selectedCameraJobId }) {
<AssetsSelector <AssetsSelector
style={{ flex: 1 }} style={{ flex: 1 }}
key={tick} key={tick}
options={{ Settings={widgetSettings}
assetsType: ["photo", "video"], Errors={widgetErrors}
margin: 3, Styles={widgetStyles}
portraitCols: 4, Navigator={widgetNavigator}
landscapeCols: 6, />
widgetWidth: 100, )}
widgetBgColor: "white", {bodyshop.uselocalmediaserver ? (
selectedBgColor: "#adadad", <LocalUploadProgress
spinnerColor: "#c8c8c8", uploads={uploads}
videoIcon: { setUploads={setUploads}
Component: Ionicons, forceRerender={forceRerender}
iconName: "ios-videocam", />
color: "white", ) : (
size: 20, <UploadProgress
}, uploads={uploads}
selectedIcon: { setUploads={setUploads}
Component: Ionicons, forceRerender={forceRerender}
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>
);
},
},
}}
/> />
)} )}
<UploadProgress uploads={uploads} forceRerender={forceRerender} />
</View> </View>
); );
} }
@@ -130,3 +189,60 @@ const styles = StyleSheet.create({
}); });
export default connect(mapStateToProps, null)(ImageBrowserScreen); 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>
// );
// },
// },
// }}

View File

@@ -1,8 +1,8 @@
import Constants from "expo-constants"; import Constants from "expo-constants";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, View, Text } from "react-native"; import { View, Text } from "react-native";
import { Title } from "react-native-paper"; import { Title, Button } from "react-native-paper";
import { purgeStoredState } from "redux-persist"; import { purgeStoredState } from "redux-persist";
import SignOutButton from "../sign-out-button/sign-out-button.component"; import SignOutButton from "../sign-out-button/sign-out-button.component";
import * as Updates from "expo-updates"; import * as Updates from "expo-updates";
@@ -25,9 +25,9 @@ export default function ScreenSettingsComponent() {
})} })}
</Title> </Title>
<Text>{Updates.releaseChannel}</Text> <Text>Release Channel {Updates.releaseChannel}</Text>
<SignOutButton /> <SignOutButton />
<Button title="Purge State" onPress={() => purgeStoredState()} /> {/* <Button title="Purge State" onPress={() => purgeStoredState()} /> */}
</View> </View>
); );
} }

View File

@@ -38,7 +38,7 @@ export function SignIn({ emailSignInStart, signingIn }) {
<View <View
style={{ style={{
display: "flex", display: "flex",
marginTop: 80,
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
justifyContent: "space-evenly", justifyContent: "space-evenly",
@@ -47,43 +47,52 @@ export function SignIn({ emailSignInStart, signingIn }) {
<Image style={localStyles.logo} source={Logo} /> <Image style={localStyles.logo} source={Logo} />
<Title>{t("app.title")}</Title> <Title>{t("app.title")}</Title>
</View> </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 <View style={{ flex: 1 }}>
label={t("signin.fields.password")} <Formik
mode="outlined" initialValues={{ email: "", password: "" }}
secureTextEntry={true} onSubmit={formSubmit}
onChangeText={handleChange("password")} >
onBlur={handleBlur("password")} {({ handleChange, handleBlur, handleSubmit, values }) => (
value={values.password} <View>
style={[localStyles.input]} <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 /> <TextInput
<Button mode="outlined" loading={signingIn} onPress={handleSubmit}> label={t("signin.fields.password")}
<Text>{t("signin.actions.signin")}</Text> mode="outlined"
</Button> secureTextEntry={true}
<Text> onChangeText={handleChange("password")}
{t("settings.labels.version", { onBlur={handleBlur("password")}
number: Constants.manifest.version, value={values.password}
})} style={[localStyles.input]}
{`${process.env.NODE_ENV || ""} ${Updates.releaseChannel || ""}`} />
</Text>
</View> <SignInErrorAlertComponent />
)} <Button
</Formik> 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> </View>
); );
} }

View File

@@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, Image, StyleSheet, View } from "react-native"; 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 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(); const { t } = useTranslation();
return ( return (
<View style={[localStyles.container]}> <View style={[localStyles.container]}>
@@ -13,7 +14,17 @@ export default function ScreenSplash() {
<Title>{t("app.title")}</Title> <Title>{t("app.title")}</Title>
</View> </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> </View>
); );
} }
@@ -28,7 +39,7 @@ const localStyles = StyleSheet.create({
logoContainer: { logoContainer: {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
margin: 10,
alignItems: "center", alignItems: "center",
}, },
logo: { width: 175, height: 175, margin: 20 }, logo: { width: 175, height: 175, margin: 20 },

View File

@@ -16,6 +16,7 @@ export function SignOutButton({ signOutStart }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Button <Button
style={{ margin: 8 }}
onPress={() => signOutStart()} onPress={() => signOutStart()}
title={t("general.actions.signout")} title={t("general.actions.signout")}
/> />

View File

@@ -1,19 +1,18 @@
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library"; import * as MediaLibrary from "expo-media-library";
import _ from "lodash";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
Modal, Modal,
ScrollView, Platform,
StyleSheet, StyleSheet,
Text, Text,
View, View,
} from "react-native"; } from "react-native";
import { ProgressBar } from "react-native-paper"; import { Divider, ProgressBar } from "react-native-paper";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.analytics"; import { logImEXEvent } from "../../firebase/firebase.analytics";
@@ -27,6 +26,8 @@ import {
selectCurrentUser, selectCurrentUser,
} from "../../redux/user/user.selectors"; } from "../../redux/user/user.selectors";
import { formatBytes, handleUpload } from "../../util/document-upload.utility"; import { formatBytes, handleUpload } from "../../util/document-upload.utility";
import Toast from "react-native-toast-message";
import * as Sentry from "sentry-expo";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -43,12 +44,17 @@ export function UploadProgress({
selectedCameraJobId, selectedCameraJobId,
deleteAfterUpload, deleteAfterUpload,
uploads, uploads,
setUploads,
forceRerender, forceRerender,
}) { }) {
const [progress, setProgress] = useState({ const [progress, setProgress] = useState({
loading: false,
uploadInProgress: 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 files: {}, //uri is the key, value is progress
}); });
@@ -58,63 +64,79 @@ export function UploadProgress({
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
//Set the state of uploads to do. if (uploads) {
onDone(uploads);
if (uploads) onDone(uploads); setUploads(null);
}
}, [uploads]); }, [uploads]);
//if (!uploads) return null; function handleOnSuccess(asset) {
//NEEDS REDO.
function handleOnSuccess(id) { filesToDelete.push(asset);
logImEXEvent("imexmobile_successful_upload");
filesToDelete.push(id);
setProgress((progress) => ({ setProgress((progress) => ({
...progress, ...progress,
action: t("mediabrowser.labels.converting"), // totalUploaded: progress.totalToUpload + asset.size,
totalFilesCompleted: progress.totalFilesCompleted + 1,
files: { files: {
...progress.files, ...progress.files,
[id]: { [asset.uri]: {
...progress.files[id], ...progress.files[asset.uri],
action: t("mediabrowser.labels.converting"), uploadEnd: new Date(),
},
},
// });
}));
}
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,
}, },
}, },
})); }));
} }
function handleOnError(...props) {
logImEXEvent("imexmobile_upload_documents_error", { props });
}
const onDone = async (data) => { function handleOnProgress({ uri, filename }, percent, loaded) {
//Validate to make sure the totals for the file sizes do not exceed the total on the job. //NEED REDO
setProgress({ setProgress((progress) => {
files: _.keyBy(data, "id"), return {
loading: true, ...progress,
uploadInProgress: true, 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") { if (selectedCameraJobId !== "temp") {
const queryData = await client.query({ const queryData = await client.query({
@@ -124,11 +146,6 @@ export function UploadProgress({
jobId: selectedCameraJobId, 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 ( if (
bodyshop.jobsizelimit - bodyshop.jobsizelimit -
@@ -140,7 +157,7 @@ export function UploadProgress({
...progress, ...progress,
speed: 0, speed: 0,
action: null, action: null,
loading: false, statusText: null,
uploadInProgress: false, uploadInProgress: false,
})); }));
Alert.alert( Alert.alert(
@@ -150,10 +167,22 @@ export function UploadProgress({
return; return;
} }
} }
//We made it this far. We have enough space, so let's start uploading.
//Sequentially await the proms. 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) { for (var i = 0; i < data.length + 4; i = i + 4) {
//Reset the files.
setProgress((progress) => ({ ...progress, files: {} }));
let proms = []; let proms = [];
if (data[i]) { if (data[i]) {
proms.push(CreateUploadProm(data[i])); proms.push(CreateUploadProm(data[i]));
@@ -171,38 +200,57 @@ export function UploadProgress({
await Promise.all(proms); await Promise.all(proms);
} }
//Everything is uploaded, delete the succesful ones.
if (deleteAfterUpload) { if (deleteAfterUpload) {
try { 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) { } catch (error) {
console.log("Unable to delete picture.", error); console.log("Unable to delete picture.", error);
Sentry.Native.captureException(error);
} }
} }
filesToDelete = []; filesToDelete = [];
setProgress({ Toast.show({
loading: false, type: "success",
speed: 0, text1: ` Upload completed.`,
action: null, //
// text2: duration,
});
//Reset state.
setProgress({
uploadInProgress: false, uploadInProgress: false,
files: {}, //uri is the key, value is progress totalToUpload: 0,
totalUploaded: 0,
totalFilesCompleted: 0,
startTime: null,
totalFiles: 0,
currentFile: null,
files: {},
}); });
forceRerender(); forceRerender();
}; };
const CreateUploadProm = async (p) => { const CreateUploadProm = async (p) => {
let filename; return handleUpload(
filename = p.filename || p.uri.split("/").pop();
await handleUpload(
{ {
filename,
mediaId: p.id, mediaId: p.id,
onError: handleOnError, onError: handleOnError,
onProgress: ({ percent, loaded }) => onProgress: ({ percent, loaded }) =>
handleOnProgress(p.id, percent, loaded), handleOnProgress(p, percent, loaded),
onSuccess: () => handleOnSuccess(p.id), onSuccess: () => handleOnSuccess(p),
}, },
{ {
bodyshop: bodyshop, bodyshop: bodyshop,
@@ -211,20 +259,6 @@ export function UploadProgress({
photo: p, 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 ( return (
@@ -233,20 +267,31 @@ export function UploadProgress({
animationType="slide" animationType="slide"
transparent={true} transparent={true}
onRequestClose={() => { 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}> <View style={styles.modalContainer}>
{progress.loading && <ActivityIndicator />} <View style={styles.modal}>
{progress.action && (
<Text>{`${progress.action} ${
(progress.speed !== 0 || !progress.speed) &&
`- ${formatBytes(progress.speed)}/sec`
}`}</Text>
)}
<ScrollView contentContainerStyle={styles.centeredView}>
{Object.keys(progress.files).map((key) => ( {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}> <Text style={styles.progressText}>
{progress.files[key].filename} {progress.files[key].filename}
</Text> </Text>
@@ -256,19 +301,59 @@ export function UploadProgress({
style={styles.progress} style={styles.progress}
color={progress.files[key].percent === 1 ? "green" : "blue"} 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>
</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> </View>
</Modal> </Modal>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
modal: { modalContainer: {
display: "flex",
flex: 1, flex: 1,
marginTop: 50, justifyContent: "center",
marginBottom: 60, },
modal: {
//flex: 1,
display: "flex",
marginLeft: 20, marginLeft: 20,
marginRight: 20, marginRight: 20,
backgroundColor: "white", backgroundColor: "white",
@@ -284,9 +369,8 @@ const styles = StyleSheet.create({
elevation: 5, elevation: 5,
}, },
centeredView: { centeredView: {
flex: 1, justifyContent: "center",
// justifyContent: "center", alignItems: "center",
// alignItems: "center",
marginTop: 22, marginTop: 22,
}, },
progressItem: { progressItem: {

22
eas.json Normal file
View 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": {}
}
}

View File

@@ -2,11 +2,17 @@ import firebase from "firebase/app";
import "firebase/auth"; import "firebase/auth";
import env from "../env"; import env from "../env";
if (!firebase.apps.length) { import { initializeApp } from "firebase/app";
firebase.initializeApp(env.firebase); 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 const analytics = firebase.analytics();
export default firebase; export default firebase;
@@ -19,14 +25,3 @@ export const getCurrentUser = () => {
}, reject); }, reject);
}); });
}; };
export const updateCurrentUser = (userDetails) => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged((userAuth) => {
userAuth.updateProfile(userDetails).then((r) => {
unsubscribe();
resolve(userAuth);
});
}, reject);
});
};

View File

@@ -6,13 +6,11 @@ export const QUERY_BODYSHOP = gql`
id id
jobsizelimit jobsizelimit
md_ro_statuses md_ro_statuses
md_order_statuses uselocalmediaserver
localmediaserverhttp
shopname shopname
messagingservicesid features
md_referral_sources localmediatoken
md_messaging_presets
md_parts_locations
md_notes_presets
} }
} }
`; `;

View File

@@ -71,9 +71,8 @@ const link = split(
// "##Intercepted GQL Transaction : " + // "##Intercepted GQL Transaction : " +
// definition.operation + // definition.operation +
// "|" + // "|" +
// definition.name.value + // // definition.name.value +
// "##", // "##"
// query
// ); // );
return ( return (
definition.kind === "OperationDefinition" && definition.kind === "OperationDefinition" &&
@@ -128,11 +127,16 @@ export const client = new ApolloClient({
//link: from([apolloLogger, errorLink, authLink, link]), //link: from([apolloLogger, errorLink, authLink, link]),
link: from([authLink, link]), link: from([authLink, link]),
cache, cache,
notifyOnNetworkStatusChange: true,
// connectToDevTools: process.env.NODE_ENV !== "production", // connectToDevTools: process.env.NODE_ENV !== "production",
defaultOptions: { defaultOptions: {
watchQuery: { watchQuery: {
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
},
query: {
fetchPolicy: "network-only",
}, },
}, },
}); });

View File

@@ -183,7 +183,7 @@ export const GET_JOB_BY_PK = gql`
date_exported date_exported
status status
owner_owing owner_owing
joblines { joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) {
id id
unq_seq unq_seq
line_ind line_ind

6
metro.config.js Normal file
View File

@@ -0,0 +1,6 @@
const { getDefaultConfig } = require("metro-config");
const { resolver: defaultResolver } = getDefaultConfig.getDefaultValues();
exports.resolver = {
...defaultResolver,
sourceExts: [...defaultResolver.sourceExts, "cjs", "jsx"],
};

View File

@@ -8,69 +8,86 @@
"eject": "expo eject", "eject": "expo eject",
"release:test": "expo publish --release-channel test", "release:test": "expo publish --release-channel test",
"release:production": "expo publish --release-channel production", "release:production": "expo publish --release-channel production",
"build:ios:production": "expo build:ios --release-channel production", "build:production": "eas build --profile production",
"build:ios:test": "expo build:ios --release-channel test", "build:test": "eas build --profile test",
"build:android:production": "expo build:android --release-channel production", "build:test:local:ios": "eas build --profile test --platform ios --local",
"build:android:test": "expo build:android --release-channel test" "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": { "dependencies": {
"@apollo/client": "^3.3.19", "@apollo/client": "^3.7.0-alpha.3",
"@expo/vector-icons": "^12.0.0", "@expo/vector-icons": "^13.0.0",
"@react-native-async-storage/async-storage": "^1.13.0", "@react-native-async-storage/async-storage": "~1.17.6",
"@react-native-community/art": "^1.2.0", "@react-native-community/art": "^1.2.0",
"@react-native-community/masked-view": "0.1.10", "@react-native-community/cli-debugger-ui": "^7.0.3",
"@react-navigation/bottom-tabs": "^5.11.11", "@react-native-community/masked-view": "^0.1.11",
"@react-navigation/drawer": "^5.12.5", "@react-navigation/bottom-tabs": "^6.2.0",
"@react-navigation/native": "^5.9.4", "@react-navigation/drawer": "^6.3.1",
"@react-navigation/stack": "^5.14.5", "@react-navigation/native": "^6.0.8",
"axios": "^0.21.0", "@react-navigation/stack": "^6.1.1",
"cloudinary-core": "^2.11.4", "axios": "^0.27.2",
"dinero.js": "^1.8.1", "cloudinary-core": "^2.12.3",
"expo": "^41.0.0", "dinero.js": "^1.9.1",
"expo-app-loading": "^1.0.3", "expo": "^45.0.5",
"expo-av": "~9.1.2", "expo-app-loading": "~2.0.0",
"expo-camera": "~11.0.2", "expo-application": "~4.1.0",
"expo-file-system": "~11.0.2", "expo-av": "~11.2.3",
"expo-firebase-analytics": "~4.0.2", "expo-camera": "~12.2.0",
"expo-font": "~9.1.0", "expo-constants": "~13.1.1",
"expo-images-picker": "git+https://github.com/snaptsoft/expo-images-picker/", "expo-dev-client": "~1.0.0",
"expo-localization": "~10.1.0", "expo-device": "~4.2.0",
"expo-media-library": "~12.0.2", "expo-file-system": "~14.0.0",
"expo-permissions": "~12.0.1", "expo-firebase-analytics": "~7.0.0",
"expo-status-bar": "~1.0.4", "expo-font": "~10.1.0",
"expo-video-thumbnails": "~5.1.0", "expo-image-manipulator": "~10.3.1",
"firebase": "8.2.3", "expo-images-picker": "^2.4.1",
"formik": "^2.2.8", "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", "graphql": "^15.4.0",
"i18next": "^20.3.1", "i18next": "^21.8.10",
"intl": "^1.2.5",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"luxon": "^1.27.0", "luxon": "^2.3.1",
"react": "16.13.1", "mime": "^3.0.0",
"react-dom": "16.13.1", "moment": "^2.29.1",
"react-i18next": "^11.10.0", "normalize-url": "^7.0.3",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz", "react": "17.0.2",
"react-native-gesture-handler": "~1.10.2", "react-dom": "17.0.2",
"react-native-image-gallery": "archriss/react-native-image-gallery#152/head", "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-indicators": "^0.17.0",
"react-native-pager-view": "5.0.12", "react-native-pager-view": "5.4.15",
"react-native-paper": "^4.9.1", "react-native-paper": "^4.11.2",
"react-native-progress": "^4.1.2", "react-native-progress": "^5.0.0",
"react-native-reanimated": "~2.1.0", "react-native-reanimated": "~2.8.0",
"react-native-screens": "~3.0.0", "react-native-safe-area-context": "4.2.4",
"react-native-tab-view": "3.0.1", "react-native-screens": "~3.11.1",
"react-native-web": "~0.13.12", "react-native-tab-view": "3.1.1",
"react-redux": "^7.2.4", "react-native-toast-message": "^2.1.5",
"redux": "^4.1.0", "react-native-web": "0.17.7",
"react-redux": "^7.2.6",
"redux": "^4.1.2",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.1.3", "redux-saga": "^1.1.3",
"reselect": "^4.0.0", "reselect": "^4.1.6",
"sentry-expo": "^3.1.0", "sentry-expo": "^4.2.0",
"subscriptions-transport-ws": "^0.9.18" "subscriptions-transport-ws": "^0.9.18"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "~7.9.0", "@babel/core": "^7.12.9",
"babel-preset-expo": "8.3.0", "babel-preset-expo": "~9.1.0",
"eslint": "^7.27.0", "eslint": "^7.27.0",
"eslint-plugin-react": "^7.24.0", "eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-native": "^3.11.0" "eslint-plugin-react-native": "^3.11.0"

1
readme.md Normal file
View File

@@ -0,0 +1 @@
USE VERSION 16 of NODE LTS.

View File

@@ -1,11 +1,13 @@
import * as Analytics from "expo-firebase-analytics"; import * as Analytics from "expo-firebase-analytics";
import { signInWithEmailAndPassword, signOut } from "firebase/auth";
import { all, call, put, takeLatest } from "redux-saga/effects"; import { all, call, put, takeLatest } from "redux-saga/effects";
import * as Sentry from "sentry-expo";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { import {
auth, auth,
getCurrentUser, getCurrentUser,
updateCurrentUser, updateCurrentUser,
} from "../../firebase/firebase.utils"; } from "../../firebase/firebase.utils";
import { logImEXEvent } from "../../firebase/firebase.analytics";
import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries"; import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
import { client } from "../../graphql/client"; import { client } from "../../graphql/client";
import { import {
@@ -29,7 +31,7 @@ export function* onEmailSignInStart() {
export function* signInWithEmail({ payload: { email, password } }) { export function* signInWithEmail({ payload: { email, password } }) {
try { try {
logImEXEvent("imexmobile_sign_in_attempt", { user: email }); logImEXEvent("imexmobile_sign_in_attempt", { user: email });
const { user } = yield auth.signInWithEmailAndPassword(email, password); const { user } = yield signInWithEmailAndPassword(auth, email, password);
yield put( yield put(
signInSuccess({ signInSuccess({
uid: user.uid, uid: user.uid,
@@ -41,6 +43,7 @@ export function* signInWithEmail({ payload: { email, password } }) {
); );
} catch (error) { } catch (error) {
yield put(signInFailure(error)); yield put(signInFailure(error));
//logImEXEvent("redux_sign_in_failure", { user: email, error }); //logImEXEvent("redux_sign_in_failure", { user: email, error });
} }
} }
@@ -78,7 +81,7 @@ export function* signOutStart() {
try { try {
logImEXEvent("imexmobile_sign_out"); logImEXEvent("imexmobile_sign_out");
yield auth.signOut(); yield signOut(auth);
yield put(signOutSuccess()); yield put(signOutSuccess());
} catch (error) { } catch (error) {
yield put(signOutFailure(error.message)); yield put(signOutFailure(error.message));
@@ -105,12 +108,21 @@ export function* onSignInSuccess() {
export function* signInSuccessSaga({ payload }) { export function* signInSuccessSaga({ payload }) {
try { try {
Analytics.setUserId(payload.email); Analytics.setUserId(payload.email);
Sentry.Native.setUser({ email: payload.email });
const shop = yield client.query({ query: QUERY_BODYSHOP }); const shop = yield client.query({ query: QUERY_BODYSHOP });
logImEXEvent("imexmobile_sign_in_success", payload); logImEXEvent("imexmobile_sign_in_success", payload);
yield put(setBodyshop(shop.data.bodyshops[0])); 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) { } catch (error) {
console.log("UH-OH. Couldn't get shop details.", error); console.log("UH-OH. Couldn't get shop details.", error);
Sentry.Native.captureException(error);
} }
} }

View File

@@ -1,6 +1,7 @@
{ {
"translation": { "translation": {
"app": { "app": {
"nomobileaccess": "Your shop does not currently have access to ImEX Mobile. ",
"title": "ImEX Mobile" "title": "ImEX Mobile"
}, },
"camera": { "camera": {
@@ -77,7 +78,8 @@
"labels": { "labels": {
"activejobs": "Jobs", "activejobs": "Jobs",
"detail": "Job Detail", "detail": "Job Detail",
"nojobs": "There are no active jobs." "nojobs": "There are no active jobs.",
"search": "Search..."
}, },
"titles": { "titles": {
"jobtab": "Jobs" "jobtab": "Jobs"
@@ -91,6 +93,7 @@
"labels": { "labels": {
"converting": "Converting", "converting": "Converting",
"deleteafterupload": "Delete After Upload", "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.", "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 ---", "selectjob": "--- Select a job ---",
"selectjobassetselector": "Please select a job to upload media. ", "selectjobassetselector": "Please select a job to upload media. ",

View File

@@ -1,6 +1,7 @@
{ {
"translation": { "translation": {
"app": { "app": {
"nomobileaccess": "",
"title": "" "title": ""
}, },
"camera": { "camera": {
@@ -77,7 +78,8 @@
"labels": { "labels": {
"activejobs": "", "activejobs": "",
"detail": "", "detail": "",
"nojobs": "" "nojobs": "",
"search": ""
}, },
"titles": { "titles": {
"jobtab": "" "jobtab": ""
@@ -91,6 +93,7 @@
"labels": { "labels": {
"converting": "", "converting": "",
"deleteafterupload": "", "deleteafterupload": "",
"localserver": "",
"nomedia": "", "nomedia": "",
"selectjob": "", "selectjob": "",
"selectjobassetselector": "", "selectjobassetselector": "",

View File

@@ -1,6 +1,7 @@
{ {
"translation": { "translation": {
"app": { "app": {
"nomobileaccess": "",
"title": "" "title": ""
}, },
"camera": { "camera": {
@@ -77,7 +78,8 @@
"labels": { "labels": {
"activejobs": "", "activejobs": "",
"detail": "", "detail": "",
"nojobs": "" "nojobs": "",
"search": ""
}, },
"titles": { "titles": {
"jobtab": "" "jobtab": ""
@@ -91,6 +93,7 @@
"labels": { "labels": {
"converting": "", "converting": "",
"deleteafterupload": "", "deleteafterupload": "",
"localserver": "",
"nomedia": "", "nomedia": "",
"selectjob": "", "selectjob": "",
"selectjobassetselector": "", "selectjobassetselector": "",

View File

@@ -5,6 +5,7 @@ import { INSERT_NEW_DOCUMENT } from "../graphql/documents.queries";
import { axiosAuthInterceptorId } from "./CleanAxios"; import { axiosAuthInterceptorId } from "./CleanAxios";
import * as MediaLibrary from "expo-media-library"; import * as MediaLibrary from "expo-media-library";
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
import * as Sentry from "sentry-expo";
//Context: currentUserEmail, bodyshop, jobid, invoiceid //Context: currentUserEmail, bodyshop, jobid, invoiceid
@@ -13,24 +14,25 @@ var cleanAxios = axios.create();
cleanAxios.interceptors.request.eject(axiosAuthInterceptorId); cleanAxios.interceptors.request.eject(axiosAuthInterceptorId);
export const handleUpload = async (ev, context) => { export const handleUpload = async (ev, context) => {
const { filename, mediaId, onError, onSuccess, onProgress } = ev; const { mediaId, onError, onSuccess, onProgress } = ev;
const { bodyshop, jobId } = context; const { bodyshop, jobId } = context;
const imageData = await MediaLibrary.getAssetInfoAsync(mediaId); 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 extension = imageData.localUri.split(".").pop();
let key = `${bodyshop.id}/${jobId}/${(filename || newFile.data.name).replace( let key = `${bodyshop.id}/${jobId}/${(
/\.[^/.]+$/, imageData.filename || imageData.uri.split("/").pop()
"" ).replace(/\.[^/.]+$/, "")}-${new Date().getTime()}`;
)}-${new Date().getTime()}`;
const res = await uploadToCloudinary( const res = await uploadToCloudinary(
key, key,
mediaId, mediaId,
imageData, imageData,
extension, extension,
newFile.type, newFile.type, //Filetype
newFile, newFile, //File
onError, onError,
onSuccess, onSuccess,
onProgress, onProgress,
@@ -51,41 +53,33 @@ export const uploadToCloudinary = async (
onProgress, onProgress,
context context
) => { ) => {
const { bodyshop, jobId, billId, uploaded_by, callback, tagsArray, photo } = const { bodyshop, jobId, uploaded_by } = context;
context;
//Set variables for getting the signed URL. //Set variables for getting the signed URL.
let timestamp = Math.floor(Date.now() / 1000); let timestamp = Math.floor(Date.now() / 1000);
let public_id = key; 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") const upload_preset = fileType.startsWith("video")
? "incoming_upload_video" ? "incoming_upload_video"
: "incoming_upload"; : "incoming_upload";
//Get the signed url. //Get the signed url.
let signedURLResponse; let signedURLResponse;
try { try {
signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, { signedURLResponse = await axios.post(`${env.API_URL}/media/sign`, {
public_id: public_id, public_id: public_id,
tags: tags,
timestamp: timestamp, timestamp: timestamp,
upload_preset: upload_preset, upload_preset: upload_preset,
}); });
} catch (error) { } catch (error) {
console.log("ERROR GETTING SIGNED URL", error); console.log("ERROR GETTING SIGNED URL", error);
Sentry.Native.captureException(error);
return { success: false, error: error }; return { success: false, error: error };
} }
if (signedURLResponse.status !== 200) { if (signedURLResponse.status !== 200) {
console.log("Error Getting Signed URL", signedURLResponse.statusText); console.log("Error Getting Signed URL", signedURLResponse.statusText);
if (onError) onError(signedURLResponse.statusText); if (onError) onError(signedURLResponse.statusText);
return { success: false, error: signedURLResponse.statusText }; return { success: false, error: signedURLResponse.statusText };
} }
@@ -93,7 +87,10 @@ export const uploadToCloudinary = async (
var signature = signedURLResponse.data; var signature = signedURLResponse.data;
var options = { var options = {
headers: { "X-Requested-With": "XMLHttpRequest" }, headers: {
"X-Requested-With": "XMLHttpRequest",
"Content-Type": "multipart/form-data",
},
onUploadProgress: (e) => { onUploadProgress: (e) => {
if (onProgress) if (onProgress)
onProgress({ percent: e.loaded / e.total, loaded: e.loaded }); 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("upload_preset", upload_preset);
formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY); formData.append("api_key", env.REACT_APP_CLOUDINARY_API_KEY);
formData.append("public_id", public_id); formData.append("public_id", public_id);
formData.append("tags", tags);
formData.append("timestamp", timestamp); formData.append("timestamp", timestamp);
formData.append("signature", signature); formData.append("signature", signature);
@@ -121,13 +117,13 @@ export const uploadToCloudinary = async (
fileType fileType
)}/upload`, )}/upload`,
formData, formData,
{ options
...options,
}
); );
// console.log("Cloudinary Upload Response", cloudinaryUploadResponse.data);
} catch (error) { } catch (error) {
console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse); console.log("CLOUDINARY error", error.response, cloudinaryUploadResponse);
Sentry.Native.captureException(error);
if (onError) onError(error.message);
return { success: false, error: error }; return { success: false, error: error };
} }
@@ -144,35 +140,10 @@ export const uploadToCloudinary = async (
//Insert the document with the matching key. //Insert the document with the matching key.
const documentInsert = await client.mutate({ const documentInsert = await client.mutate({
mutation: INSERT_NEW_DOCUMENT, 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: { variables: {
docInput: [ docInput: [
{ {
...(jobId ? { jobid: jobId } : {}), ...(jobId ? { jobid: jobId } : {}),
...(billId ? { billid: billId } : {}),
uploaded_by: uploaded_by, uploaded_by: uploaded_by,
key: key, key: key,
type: fileType, type: fileType,
@@ -194,19 +165,8 @@ export const uploadToCloudinary = async (
status: "done", status: "done",
key: documentInsert.data.insert_documents.returning[0].key, key: documentInsert.data.insert_documents.returning[0].key,
}); });
// notification["success"]({
// message: i18n.t("documents.successes.insert"),
// });
if (callback) {
callback();
}
} else { } else {
if (onError) onError(JSON.stringify(documentInsert.errors)); if (onError) onError(JSON.stringify(documentInsert.errors));
// notification["error"]({
// message: i18n.t("documents.errors.insert", {
// message: JSON.stringify(JSON.stringify(documentInsert.errors)),
// }),
// });
return { return {
success: false, success: false,
error: JSON.stringify(documentInsert.errors), error: JSON.stringify(documentInsert.errors),
@@ -231,6 +191,7 @@ export function formatBytes(a, b = 2) {
if (0 === a || !a) return "0 Bytes"; if (0 === a || !a) return "0 Bytes";
const c = 0 > b ? 0 : b, const c = 0 > b ? 0 : b,
d = Math.floor(Math.log(a) / Math.log(1024)); d = Math.floor(Math.log(a) / Math.log(1024));
return ( return (
parseFloat((a / Math.pow(1024, d)).toFixed(c)) + parseFloat((a / Math.pow(1024, d)).toFixed(c)) +
" " + " " +

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

10367
yarn.lock

File diff suppressed because it is too large Load Diff