ldc-com-app/ComApp/src/App/Views/SyncScreen.js

511 lines
14 KiB
JavaScript
Raw Normal View History

2022-10-05 18:03:52 +02:00
import React from 'react';
import { Alert, Button, Linking, Text, View } from 'react-native';
2022-10-05 18:03:52 +02:00
// Redux
import { connect } from 'react-redux';
2022-12-28 17:53:43 +01:00
import { initContact } from '../Store/Actions/contacts';
import { initDirectories } from '../Store/Actions/directories';
2023-01-04 15:28:10 +01:00
import { initDocuments } from '../Store/Actions/documents';
// Sentry
import * as Sentry from '@sentry/react-native';
2023-09-05 09:27:14 +02:00
2022-10-05 18:03:52 +02:00
// Style
import styles from '../../Resources/Styles/SyncStyle';
// Translations
import i18n from '../../I18n';
// Controller
2022-10-05 18:03:52 +02:00
import SyncController from '../Controllers/SyncController';
// Progress
import * as Progress from 'react-native-progress';
// Database
import {
Contact,
Directory,
Document,
Form,
SendBrochure,
User,
} from '../../Database';
2022-10-06 16:25:15 +02:00
// FileSytem
import * as FileSystem from 'expo-file-system';
2022-10-05 18:03:52 +02:00
function mapStateToProps(state) {
return {
syncVersion: state.app.syncVersion,
currentUser: state.app.currentUser,
locale: state.locale.locale,
mainColor: state.app.mainColor,
directories: state.directories,
};
2022-10-05 18:03:52 +02:00
}
2022-12-28 16:32:30 +01:00
const mapDispatchToProps = dispatch => {
return {
init: () => dispatch({ type: 'INIT' }),
initUsers: () => dispatch({ type: 'INIT_USERS' }),
initFormularies: () => dispatch({ type: 'INIT_FORMS' }),
initDirectories: (lang, user) => dispatch(initDirectories(lang, user)),
initDocuments: (lang, directories) =>
dispatch(initDocuments(lang, directories)),
initContact: currentUser => dispatch(initContact(currentUser)),
initBtns: () => dispatch({ type: 'INIT_HOME_BTNS' }),
updateVersion: version =>
dispatch({
type: 'UPDATE_VERSION',
data: version,
}),
updateShowIsActive: value =>
dispatch({
type: 'UPDATE_SHOW_IS_ACTIVE',
data: value,
}),
};
2022-12-28 16:32:30 +01:00
};
2022-10-05 18:03:52 +02:00
class SyncScreen extends React.Component {
state = {
progress: 0,
txtStep: '',
downloadProgress: 0,
downloadingFile: false,
processing: false,
newApp: false,
forced: false,
};
2022-10-05 18:03:52 +02:00
componentDidMount() {
const { forceSync } = this.props.route.params;
if (forceSync === true) {
this.setState({ forced: true });
this.sync();
2022-10-07 18:27:54 +02:00
}
}
2022-10-07 18:27:54 +02:00
render() {
const fileProgress = this.fileProgress();
return (
<View style={styles.container}>
<Progress.Circle
size={250}
progress={this.state.progress}
showsText={true}
color={this.props.mainColor}
/>
<View style={styles.btnWrapper}>
<Button
disabled={this.state.processing}
color={this.props.mainColor}
title={i18n.t('sync_s.start_sync')}
onPress={() => this.sync()}
/>
</View>
<View style={styles.btnWrapper}>
<Text>
{this.state.txtStep} {fileProgress}
</Text>
</View>
</View>
);
}
2022-10-05 18:03:52 +02:00
async sync() {
let errorStatus = false;
console.debug('Sync');
/**
* @param {Promise<import('@react-native-community/netinfo').NetInfoState>} promise
* @param {(error:any) => void|null} onError
*/
const getCall = (promise, onError = null) => {
if (onError === null) {
/** @param {import('axios').AxiosError} error */
onError = error => {
let message = 'Erreur de la communication serveur';
if (error?.response?.status === 422) {
message =
this.parseValidationErrors(error.response?.data?.errors ?? {}) ||
message;
console.warn(error.response?.data?.errors ?? {});
} else if (error?.response?.status === 419) {
// * CSRF TOKEN MISMATCH (On ne pourrais plus faire d'appels AJAX sans recharger la page)
message = 'Votre session a expiré, la page va être rechargée';
} else if (error?.response?.status === 403) {
// * Access denied
message = "Vous n'êtes pas autorisé a effectuer cette action";
} else {
console.error(error);
}
console.warn(message);
Sentry.captureException(error);
Alert.alert(
i18n.t('error.error'),
i18n.t('error.error_has_occurred'),
);
errorStatus = true;
};
}
return promise.catch(onError);
};
const isConnected = await SyncController.checkConnection().catch(
error => false,
);
if (!isConnected) {
Alert.alert(
i18n.t('global.warning'),
i18n.t('error.must_be_connected_internet_sync'),
);
return;
2022-10-05 18:03:52 +02:00
}
this.setState({ processing: true });
// * --- getVersion
const resVersion = await getCall(
SyncController.getVersion().then(res => {
return res.data;
}),
);
if (errorStatus) {
return false;
2022-10-05 18:03:52 +02:00
}
this.props.updateVersion(resVersion.version);
this.props.updateShowIsActive(resVersion.ShowIsActive);
this.setState({ newApp: resVersion.newApp });
// * --- getUsers
this.setState({
progress: 0.1,
txtStep: i18n.t('sync_s.updating_users') + '...',
});
const resUsers = await getCall(
SyncController.getUsers().then(res => {
User.clear();
return res.data;
}),
);
if (errorStatus) {
return false;
}
resUsers.forEach(user => {
User.add(user);
});
// * --- Contacts Sync
/*
* This is done only for actual app user
- 1 Upsert all contacts
- 2 Delete contacts where to_delete is true
- 3 Get all contacts
*/
this.setState({
progress: 0.2,
txtStep: i18n.t('sync_s.updating_contacts') + '...',
});
// * --- Contacts Sync -- STEP 1
await getCall(SyncController.upsertContacts(this.props.currentUser.id));
if (errorStatus) {
return false;
}
// * --- Contacts Sync -- STEP 2
this.setState({
progress: 0.23,
});
await getCall(SyncController.deleteContacts(this.props.currentUser.id));
if (errorStatus) {
return false;
}
// * --- Contacts Sync -- STEP 3
this.setState({
progress: 0.26,
});
const resContacts = await getCall(
SyncController.getContacts(this.props.currentUser.id).then(res => {
// * Clear Contacts, in order to fetch them all after.
Contact.clear(this.props.currentUser.id);
return res.data;
}),
);
if (errorStatus) {
return false;
}
/** @type {(contact: import('./../Models/Contact').ContactData ) => void} */
const contactMapper = contact => {
// * Stringify complex Fields
contact.data = JSON.stringify(contact.data);
contact.visits = JSON.stringify(contact.visits);
contact.timestamp = new Date(contact.timestamp);
Contact.set(contact);
};
resContacts.forEach(contactMapper);
// * --- sendBrochures
this.setState({
progress: 0.3,
txtStep: i18n.t('sync_s.sending_flyers') + '...',
});
const brochures = SendBrochure.getAll();
for (const brochure of brochures) {
await getCall(
SyncController.sendBrochure(brochure).then(res => {
SendBrochure.delete(brochure.id);
}),
);
2022-10-06 16:25:15 +02:00
}
if (errorStatus) {
return false;
}
// * --- getDirectories
this.setState({
progress: 0.4,
txtStep: i18n.t('sync_s.updating_folders') + '...',
});
const resDirectories = await getCall(
SyncController.getDirectories().then(res => {
Directory.clear();
return res.data;
}),
);
if (errorStatus) {
return false;
}
resDirectories.forEach(dir => {
Directory.add(dir);
});
// * --- getFormularies
this.setState({
progress: 0.5,
txtStep: i18n.t('sync_s.updating_forms') + '...',
});
const resFormularies = await getCall(
SyncController.getFormularies().then(res => {
Form.clear();
return res.data;
}),
);
if (errorStatus) {
return false;
}
resFormularies.forEach(form => Form.add(form));
// * --- getDocuments
this.setState({
progress: 0.6,
txtStep: i18n.t('sync_s.updating_docs') + '...',
});
const resDocuments = await getCall(
SyncController.getDocuments().then(res => {
return res.data;
}),
);
if (errorStatus) {
return false;
2022-10-06 16:25:15 +02:00
}
const toDelete = Document.clear(resDocuments);
resDocuments.forEach(document => {
Document.update(document);
});
this.downloadDocuments(Document.getAll(), toDelete);
// * --- End
}
2022-10-06 16:25:15 +02:00
fileProgress() {
if (this.state.downloadingFile === true) {
return this.state.downloadProgress + ' %';
2022-10-06 16:25:15 +02:00
}
}
2022-10-06 16:25:15 +02:00
downloadDocuments(docs, toDelete) {
const version = this.props.syncVersion;
this.getToDL(docs, 0).then(toDownload => {
if (toDownload.length > 0) {
this.setState({ progress: 0.6 });
const docProgress = 0.4 / toDownload.length;
this.download(toDownload, docProgress, 0).then(status => {
if (status === true) {
this.deleteFile(toDelete, 0).then(delStatus => {
this.finishSync(version);
});
} else {
this.setState({
txtStep: i18n.t('error.error_has_occurred') + '.',
});
}
2022-10-06 16:25:15 +02:00
});
} else {
this.deleteFile(toDelete, 0).then(status => {
this.setState({ progress: 1 });
this.finishSync(version);
});
}
});
}
2022-10-06 16:25:15 +02:00
async getToDL(docs, i, toDL = []) {
if (docs.length < 1) {
return toDL;
2022-10-06 16:25:15 +02:00
}
const item = docs[i];
const { exists, modificationTime } = await FileSystem.getInfoAsync(
FileSystem.documentDirectory + item.uuid + '.' + item.type,
);
if (exists === false) {
toDL.push(item);
} else {
if (modificationTime.toString() !== item.updated_at) {
toDL.push(item);
}
}
if (i < docs.length - 1) {
return this.getToDL(docs, i + 1, toDL);
}
return toDL;
}
2022-10-06 16:25:15 +02:00
async deleteFile(docs, i) {
if (docs.length > 0) {
const file = docs[i];
await FileSystem.deleteAsync(
FileSystem.documentDirectory + file.uuid + '.' + file.type,
{ idempotent: true },
);
if (i < docs.length - 1) {
return this.deleteFile(docs, i + 1);
}
2022-10-06 16:25:15 +02:00
}
return true;
}
callback(downloadProgress) {
let progress =
(downloadProgress.totalBytesWritten /
downloadProgress.totalBytesExpectedToWrite) *
100;
progress = Math.round(progress);
progress = progress.toString();
this.setState({
downloadProgress: progress,
});
}
2022-10-06 16:25:15 +02:00
downloadResumable(url, uuid, type) {
return FileSystem.createDownloadResumable(
url,
FileSystem.documentDirectory + uuid + '.' + type,
{},
this.callback.bind(this),
);
}
async download(documents, progress, i) {
if (this.state.downloadingFile === false) {
this.setState({ downloadingFile: true });
}
const tenantUrl = SyncController.getUrlBo();
const rootUrl = tenantUrl.substring(0, tenantUrl.lastIndexOf('/'));
const tenantId = tenantUrl.match(/[^\/]+$/)[0];
const endpoint = rootUrl + '/storage/docs/' + tenantId + "/";
const doc = documents[i];
this.setState({
txtStep:
i18n.t('sync_s.download') +
' : ' +
doc.title +
' (' +
doc.type.toUpperCase() +
') ...',
});
try {
2023-09-05 09:27:14 +02:00
const uri = `${endpoint}${doc.uuid}.${doc.type}`;
await this.downloadResumable(
endpoint + doc.uuid + '.' + doc.type,
doc.uuid,
doc.type,
).downloadAsync();
const { modificationTime } = await FileSystem.getInfoAsync(
FileSystem.documentDirectory + doc.uuid + '.' + doc.type,
);
SyncController.updateLastModificationDoc(doc.id, modificationTime);
2023-09-05 09:27:14 +02:00
console.debug('Finished downloading to ', uri);
this.setState({ progress: this.state.progress + progress });
if (i < documents.length - 1) {
return this.download(documents, progress, i + 1);
}
this.setState({ downloadingFile: false });
return true;
} catch (e) {
console.error(e);
return false;
2022-10-06 16:25:15 +02:00
}
}
2022-10-06 16:25:15 +02:00
finishSync(version) {
this.props.init();
this.props.initUsers();
this.props.initContact(this.props.currentUser);
this.props.initDirectories(this.props.locale, this.props.currentUser);
this.props.initDocuments(this.props.locale, this.props.directories.global);
this.props.initBtns();
if (this.state.forced) {
SyncController.setInstalled();
}
SyncController.updateVersion(version);
this.setState({
txtStep: i18n.t('sync_s.update_completed') + '.',
processing: false,
});
if (this.state.newApp === true) {
const appUUID = SyncController.getAppStoreUUID();
const url = 'https://store.ldc.fr' + '/app/' + appUUID;
Alert.alert(
'LDC Store',
i18n.t('sync_s.new_app_version'),
[
{
text: i18n.t('global.cancel'),
2023-09-05 09:27:14 +02:00
onPress: () => console.debug('Cancel Pressed'),
style: 'cancel',
},
{
text: i18n.t('sync_s.open_store'),
onPress: () =>
Linking.openURL(url).catch(err =>
console.error('An error occurred', err),
),
},
],
{ cancelable: false },
);
}
}
/**
* Parse error messages for a specific input.
*
* @param {Record<string, Array<string>>} allErrors
* @return {string | null}
*/
parseValidationErrors(allErrors = {}) {
const errorList = Object.values(allErrors);
if (!errorList.length) {
return null;
}
return errorList
.reduce((oldInputErrors, inputErrors) =>
oldInputErrors ? oldInputErrors.concat(inputErrors) : inputErrors,
)
.reduce(
(oldInputString, inputString) =>
(oldInputString ? `${oldInputString}, ` : '') + inputString,
);
}
2022-10-05 18:03:52 +02:00
}
export default connect(mapStateToProps, mapDispatchToProps)(SyncScreen);