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
|
# macOS
|
||||||
.DS_Store
|
.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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
app.json
25
app.json
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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 { 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>
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
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 "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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
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",
|
"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,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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. ",
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -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)) +
|
||||||
" " +
|
" " +
|
||||||
|
|||||||
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