diff --git a/www/__mocks__/cordovaMocks.ts b/www/__mocks__/cordovaMocks.ts index 1d3934ea4..1f563c7e7 100644 --- a/www/__mocks__/cordovaMocks.ts +++ b/www/__mocks__/cordovaMocks.ts @@ -29,6 +29,7 @@ export const mockFile = () => { window['cordova'].file = { dataDirectory: '../path/to/data/directory', applicationStorageDirectory: '../path/to/app/storage/directory', + tempDirectory: '../path/to/temp/directory', }; }; diff --git a/www/__mocks__/fileSystemMocks.ts b/www/__mocks__/fileSystemMocks.ts index 70b532507..1648c2f4b 100644 --- a/www/__mocks__/fileSystemMocks.ts +++ b/www/__mocks__/fileSystemMocks.ts @@ -1,4 +1,9 @@ export const mockFileSystem = () => { + type MockFileWriter = { + onreadend: any; + onerror: (e: any) => void; + write: (obj: Blob) => void; + }; window['resolveLocalFileSystemURL'] = function (parentDir, handleFS) { const fs = { filesystem: { @@ -9,6 +14,8 @@ export const mockFileSystem = () => { let file = new File(['this is a mock'], 'loggerDB'); handleFile(file); }, + nativeURL: 'file:///Users/Jest/test/URL/', + isFile: true, }; onSuccess(fileEntry); }, diff --git a/www/index.html b/www/index.html index 72c75eb01..44fcb5bbf 100644 --- a/www/index.html +++ b/www/index.html @@ -15,4 +15,4 @@
- + \ No newline at end of file diff --git a/www/js/control/DataDatePicker.tsx b/www/js/control/DataDatePicker.tsx index 7f143f3bd..53fcff0a1 100644 --- a/www/js/control/DataDatePicker.tsx +++ b/www/js/control/DataDatePicker.tsx @@ -4,12 +4,10 @@ import React from 'react'; import { DatePickerModal } from 'react-native-paper-dates'; import { useTranslation } from 'react-i18next'; -import { getAngularService } from '../angular-react-helper'; +import { getMyData } from '../services/controlHelper'; const DataDatePicker = ({ date, setDate, open, setOpen, minDate }) => { const { t, i18n } = useTranslation(); //able to pull lang from this - const ControlHelper = getAngularService('ControlHelper'); - const onDismiss = React.useCallback(() => { setOpen(false); }, [setOpen]); @@ -18,7 +16,7 @@ const DataDatePicker = ({ date, setDate, open, setOpen, minDate }) => { (params) => { setOpen(false); setDate(params.date); - ControlHelper.getMyData(params.date); + getMyData(params.date); }, [setOpen, setDate], ); diff --git a/www/js/control/ProfileSettings.jsx b/www/js/control/ProfileSettings.jsx index 914c97d82..ae42904d8 100644 --- a/www/js/control/ProfileSettings.jsx +++ b/www/js/control/ProfileSettings.jsx @@ -33,6 +33,7 @@ import { storageClear } from '../plugin/storage'; import { getAppVersion } from '../plugin/clientStats'; import { getConsentDocument } from '../splash/startprefs'; import { logDebug } from '../plugin/logger'; +import { fetchOPCode, getSettings } from '../services/controlHelper'; //any pure functions can go outside const ProfileSettings = () => { @@ -44,7 +45,6 @@ const ProfileSettings = () => { //angular services needed const NotificationScheduler = getAngularService('NotificationScheduler'); - const ControlHelper = getAngularService('ControlHelper'); //functions that come directly from an Angular service const editCollectionConfig = () => setEditCollectionVis(true); @@ -215,7 +215,7 @@ const ProfileSettings = () => { }, [editSync]); async function getConnectURL() { - ControlHelper.getSettings().then( + getSettings().then( function (response) { var newConnectSettings = {}; newConnectSettings.url = response.connectUrl; @@ -230,7 +230,7 @@ const ProfileSettings = () => { async function getOPCode() { const newAuthSettings = {}; - const opcode = await ControlHelper.getOPCode(); + const opcode = await fetchOPCode(); if (opcode == null) { newAuthSettings.opcode = 'Not logged in'; } else { diff --git a/www/js/services/controlHelper.ts b/www/js/services/controlHelper.ts new file mode 100644 index 000000000..9969327be --- /dev/null +++ b/www/js/services/controlHelper.ts @@ -0,0 +1,159 @@ +import { DateTime } from 'luxon'; + +import { getRawEntries } from './commHelper'; +import { logInfo, displayError, logDebug, logWarn } from '../plugin/logger'; +import { FsWindow } from '../types/fileShareTypes'; +import { ServerResponse } from '../types/serverData'; +import i18next from '../i18nextInit'; + +declare let window: FsWindow; + +export const getMyDataHelpers = function ( + fileName: string, + startTimeString: string, + endTimeString: string, +) { + const localWriteFile = function (result: ServerResponse) { + const resultList = result.phone_data; + return new Promise(function (resolve, reject) { + window['resolveLocalFileSystemURL'](window['cordova'].file.cacheDirectory, function (fs) { + fs.filesystem.root.getFile( + fileName, + { create: true, exclusive: false }, + function (fileEntry) { + logDebug(`fileEntry ${fileEntry.nativeURL} is file? ${fileEntry.isFile.toString()}`); + fileEntry.createWriter(function (fileWriter) { + fileWriter.onwriteend = function () { + logDebug('Successful file write...'); + resolve(); + }; + fileWriter.onerror = function (e) { + logDebug(`Failed file write: ${e.toString()}`); + reject(); + }; + logDebug(`fileWriter is: ${JSON.stringify(fileWriter.onwriteend, null, 2)}`); + // if data object is not passed in, create a new blob instead. + const dataObj = new Blob([JSON.stringify(resultList, null, 2)], { + type: 'application/json', + }); + fileWriter.write(dataObj); + }); + }, + ); + }); + }); + }; + + const localShareData = function () { + return new Promise(function (resolve, reject) { + window['resolveLocalFileSystemURL'](window['cordova'].file.cacheDirectory, function (fs) { + fs.filesystem.root.getFile(fileName, null, function (fileEntry) { + logDebug(`fileEntry ${fileEntry.nativeURL} is file? ${fileEntry.isFile.toString()}`); + fileEntry.file( + function (file) { + const reader = new FileReader(); + + reader.onloadend = function () { + const readResult = this.result as string; + logDebug(`Successfull file read with ${readResult.length} characters`); + const dataArray = JSON.parse(readResult); + logDebug(`Successfully read resultList of size ${dataArray.length}`); + let attachFile = fileEntry.nativeURL; + const shareObj = { + files: [attachFile], + message: i18next.t( + 'email-service.email-data.body-data-consists-of-list-of-entries', + ), + subject: i18next.t('email-service.email-data.subject-data-dump-from-to', { + start: startTimeString, + end: endTimeString, + }), + }; + window['plugins'].socialsharing.shareWithOptions( + shareObj, + function (result) { + logDebug(`Share Completed? ${result.completed}`); // On Android, most likely returns false + logDebug(`Shared to app: ${result.app}`); + resolve(); + }, + function (msg) { + logDebug(`Sharing failed with message ${msg}`); + }, + ); + }; + reader.readAsText(file); + }, + function (error) { + displayError(error, 'Error while downloading JSON dump'); + reject(error); + }, + ); + }); + }); + }); + }; + + // window['cordova'].file.cacheDirectory is not guaranteed to free up memory, + // so it's good practice to remove the file right after it's used! + const localClearData = function () { + return new Promise(function (resolve, reject) { + window['resolveLocalFileSystemURL'](window['cordova'].file.cacheDirectory, function (fs) { + fs.filesystem.root.getFile(fileName, null, function (fileEntry) { + fileEntry.remove( + () => { + logDebug(`Successfully cleaned up file ${fileName}`); + resolve(); + }, + (err) => { + logWarn(`Error deleting ${fileName} : ${err}`); + reject(err); + }, + ); + }); + }); + }); + }; + + return { + writeFile: localWriteFile, + shareData: localShareData, + clearData: localClearData, + }; +}; + +/** + * getMyData fetches timeline data for a given day, and then gives the user a prompt to share the data + * @param timeStamp initial timestamp of the timeline to be fetched. + */ +export const getMyData = function (timeStamp: Date) { + // We are only retrieving data for a single day to avoid + // running out of memory on the phone + const endTime = DateTime.fromJSDate(timeStamp); + const startTime = endTime.startOf('day'); + const startTimeString = startTime.toFormat("yyyy'-'MM'-'dd"); + const endTimeString = endTime.toFormat("yyyy'-'MM'-'dd"); + + const dumpFile = startTimeString + '.' + endTimeString + '.timeline'; + alert(`Going to retrieve data to ${dumpFile}`); + + const getDataMethods = getMyDataHelpers(dumpFile, startTimeString, endTimeString); + + getRawEntries(null, startTime.toUnixInteger(), endTime.toUnixInteger()) + .then(getDataMethods.writeFile) + .then(getDataMethods.shareData) + .then(getDataMethods.clearData) + .then(function () { + logInfo('Share queued successfully'); + }) + .catch(function (error) { + displayError(error, 'Error sharing JSON dump'); + }); +}; + +export const fetchOPCode = () => { + return window['cordova'].plugins.OPCodeAuth.getOPCode(); +}; + +export const getSettings = () => { + return window['cordova'].plugins.BEMConnectionSettings.getSettings(); +}; diff --git a/www/js/types/diaryTypes.ts b/www/js/types/diaryTypes.ts index 7cce67923..9a856b115 100644 --- a/www/js/types/diaryTypes.ts +++ b/www/js/types/diaryTypes.ts @@ -42,7 +42,7 @@ export type CompositeTrip = { confirmed_trip: ObjectId; distance: number; duration: number; - end_confirmed_place: ConfirmedPlace; + end_confirmed_place: ServerData; end_fmt_time: string; end_loc: { type: string; coordinates: number[] }; end_local_dt: LocalDt; @@ -59,7 +59,7 @@ export type CompositeTrip = { raw_trip: ObjectId; sections: any[]; // TODO source: string; - start_confirmed_place: ConfirmedPlace; + start_confirmed_place: ServerData; start_fmt_time: string; start_loc: { type: string; coordinates: number[] }; start_local_dt: LocalDt;